github.com/afking/bazel-gazelle@v0.0.0-20180301150245-c02bc0f529e8/internal/resolve/index.go (about)

     1  /* Copyright 2017 The Bazel Authors. All rights reserved.
     2  
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7     http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package resolve
    17  
    18  import (
    19  	"fmt"
    20  	"log"
    21  	"path"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/bazelbuild/bazel-gazelle/internal/config"
    26  	"github.com/bazelbuild/bazel-gazelle/internal/label"
    27  	bf "github.com/bazelbuild/buildtools/build"
    28  )
    29  
    30  // RuleIndex is a table of rules in a workspace, indexed by label and by
    31  // import path. Used by Resolver to map import paths to labels.
    32  type RuleIndex struct {
    33  	rules     []*ruleRecord
    34  	labelMap  map[label.Label]*ruleRecord
    35  	importMap map[importSpec][]*ruleRecord
    36  }
    37  
    38  // ruleRecord contains information about a rule relevant to import indexing.
    39  type ruleRecord struct {
    40  	rule       bf.Rule
    41  	label      label.Label
    42  	lang       config.Language
    43  	importedAs []importSpec
    44  	embedded   bool
    45  }
    46  
    47  // importSpec describes a package to be imported. Language is specified, since
    48  // different languages have different formats for their imports.
    49  type importSpec struct {
    50  	lang config.Language
    51  	imp  string
    52  }
    53  
    54  func NewRuleIndex() *RuleIndex {
    55  	return &RuleIndex{
    56  		labelMap: make(map[label.Label]*ruleRecord),
    57  	}
    58  }
    59  
    60  // AddRulesFromFile adds existing rules to the index from file
    61  // (which must not be nil).
    62  func (ix *RuleIndex) AddRulesFromFile(c *config.Config, file *bf.File) {
    63  	buildRel, err := filepath.Rel(c.RepoRoot, file.Path)
    64  	if err != nil {
    65  		log.Panicf("file not in repo: %s", file.Path)
    66  	}
    67  	buildRel = path.Dir(filepath.ToSlash(buildRel))
    68  	if buildRel == "." || buildRel == "/" {
    69  		buildRel = ""
    70  	}
    71  
    72  	for _, stmt := range file.Stmt {
    73  		if call, ok := stmt.(*bf.CallExpr); ok {
    74  			ix.addRule(call, c.GoPrefix, buildRel)
    75  		}
    76  	}
    77  }
    78  
    79  func (ix *RuleIndex) addRule(call *bf.CallExpr, goPrefix, buildRel string) {
    80  	rule := bf.Rule{Call: call}
    81  	record := &ruleRecord{
    82  		rule:  rule,
    83  		label: label.New("", buildRel, rule.Name()),
    84  	}
    85  
    86  	if _, ok := ix.labelMap[record.label]; ok {
    87  		log.Printf("multiple rules found with label %s", record.label)
    88  		return
    89  	}
    90  
    91  	kind := rule.Kind()
    92  	switch {
    93  	case isGoLibrary(kind):
    94  		record.lang = config.GoLang
    95  		if imp := rule.AttrString("importpath"); imp != "" {
    96  			record.importedAs = []importSpec{{lang: config.GoLang, imp: imp}}
    97  		}
    98  		// Additional proto imports may be added in Finish.
    99  
   100  	case kind == "proto_library":
   101  		record.lang = config.ProtoLang
   102  		for _, s := range findSources(rule, buildRel, ".proto") {
   103  			record.importedAs = append(record.importedAs, importSpec{lang: config.ProtoLang, imp: s})
   104  		}
   105  
   106  	default:
   107  		return
   108  	}
   109  
   110  	ix.rules = append(ix.rules, record)
   111  	ix.labelMap[record.label] = record
   112  }
   113  
   114  // Finish constructs the import index and performs any other necessary indexing
   115  // actions after all rules have been added. This step is necessary because
   116  // a rule may be indexed differently based on what rules are added later.
   117  //
   118  // This function must be called after all AddRulesFromFile calls but before any
   119  // findRuleByImport calls.
   120  func (ix *RuleIndex) Finish() {
   121  	ix.skipGoEmbds()
   122  	ix.buildImportIndex()
   123  }
   124  
   125  // skipGoEmbeds sets the embedded flag on Go library rules that are imported
   126  // by other Go library rules with the same import path. Note that embedded
   127  // rules may still be imported with non-Go imports. For example, a
   128  // go_proto_library may be imported with either a Go import path or a proto
   129  // path. If the library is embedded, only the proto path will be indexed.
   130  func (ix *RuleIndex) skipGoEmbds() {
   131  	for _, r := range ix.rules {
   132  		if !isGoLibrary(r.rule.Kind()) {
   133  			continue
   134  		}
   135  		importpath := r.rule.AttrString("importpath")
   136  
   137  		var embedLabels []label.Label
   138  		if embedList, ok := r.rule.Attr("embed").(*bf.ListExpr); ok {
   139  			for _, embedElem := range embedList.List {
   140  				embedStr, ok := embedElem.(*bf.StringExpr)
   141  				if !ok {
   142  					continue
   143  				}
   144  				embedLabel, err := label.Parse(embedStr.Value)
   145  				if err != nil {
   146  					continue
   147  				}
   148  				embedLabels = append(embedLabels, embedLabel)
   149  			}
   150  		}
   151  		if libraryStr, ok := r.rule.Attr("library").(*bf.StringExpr); ok {
   152  			if libraryLabel, err := label.Parse(libraryStr.Value); err == nil {
   153  				embedLabels = append(embedLabels, libraryLabel)
   154  			}
   155  		}
   156  
   157  		for _, l := range embedLabels {
   158  			embed, ok := ix.findRuleByLabel(l, r.label)
   159  			if !ok {
   160  				continue
   161  			}
   162  			if embed.rule.AttrString("importpath") != importpath {
   163  				continue
   164  			}
   165  			embed.embedded = true
   166  		}
   167  	}
   168  }
   169  
   170  // buildImportIndex constructs the map used by findRuleByImport.
   171  func (ix *RuleIndex) buildImportIndex() {
   172  	ix.importMap = make(map[importSpec][]*ruleRecord)
   173  	for _, r := range ix.rules {
   174  		if isGoProtoLibrary(r.rule.Kind()) {
   175  			protoImports := findGoProtoSources(ix, r)
   176  			r.importedAs = append(r.importedAs, protoImports...)
   177  		}
   178  		for _, imp := range r.importedAs {
   179  			if imp.lang == config.GoLang && r.embedded {
   180  				continue
   181  			}
   182  			ix.importMap[imp] = append(ix.importMap[imp], r)
   183  		}
   184  	}
   185  }
   186  
   187  type ruleNotFoundError struct {
   188  	from label.Label
   189  	imp  string
   190  }
   191  
   192  func (e ruleNotFoundError) Error() string {
   193  	return fmt.Sprintf("no rule found for import %q, needed in %s", e.imp, e.from)
   194  }
   195  
   196  type selfImportError struct {
   197  	from label.Label
   198  	imp  string
   199  }
   200  
   201  func (e selfImportError) Error() string {
   202  	return fmt.Sprintf("rule %s imports itself with path %q", e.from, e.imp)
   203  }
   204  
   205  func (ix *RuleIndex) findRuleByLabel(label label.Label, from label.Label) (*ruleRecord, bool) {
   206  	label = label.Abs(from.Repo, from.Pkg)
   207  	r, ok := ix.labelMap[label]
   208  	return r, ok
   209  }
   210  
   211  // findRuleByImport attempts to resolve an import string to a rule record.
   212  // imp is the import to resolve (which includes the target language). lang is
   213  // the language of the rule with the dependency (for example, in
   214  // go_proto_library, imp will have ProtoLang and lang will be GoLang).
   215  // from is the rule which is doing the dependency. This is used to check
   216  // vendoring visibility and to check for self-imports.
   217  //
   218  // Any number of rules may provide the same import. If no rules provide the
   219  // import, ruleNotFoundError is returned. If a rule imports itself,
   220  // selfImportError is returned. If multiple rules provide the import, this
   221  // function will attempt to choose one based on Go vendoring logic.  In
   222  // ambiguous cases, an error is returned.
   223  func (ix *RuleIndex) findRuleByImport(imp importSpec, lang config.Language, from label.Label) (*ruleRecord, error) {
   224  	matches := ix.importMap[imp]
   225  	var bestMatch *ruleRecord
   226  	var bestMatchIsVendored bool
   227  	var bestMatchVendorRoot string
   228  	var matchError error
   229  	for _, m := range matches {
   230  		if m.lang != lang {
   231  			continue
   232  		}
   233  
   234  		switch imp.lang {
   235  		case config.GoLang:
   236  			// Apply vendoring logic for Go libraries. A library in a vendor directory
   237  			// is only visible in the parent tree. Vendored libraries supercede
   238  			// non-vendored libraries, and libraries closer to from.Pkg supercede
   239  			// those further up the tree.
   240  			isVendored := false
   241  			vendorRoot := ""
   242  			parts := strings.Split(m.label.Pkg, "/")
   243  			for i := len(parts) - 1; i >= 0; i-- {
   244  				if parts[i] == "vendor" {
   245  					isVendored = true
   246  					vendorRoot = strings.Join(parts[:i], "/")
   247  					break
   248  				}
   249  			}
   250  			if isVendored && !label.New(m.label.Repo, vendorRoot, "").Contains(from) {
   251  				// vendor directory not visible
   252  				continue
   253  			}
   254  			if bestMatch == nil || isVendored && (!bestMatchIsVendored || len(vendorRoot) > len(bestMatchVendorRoot)) {
   255  				// Current match is better
   256  				bestMatch = m
   257  				bestMatchIsVendored = isVendored
   258  				bestMatchVendorRoot = vendorRoot
   259  				matchError = nil
   260  			} else if (!isVendored && bestMatchIsVendored) || (isVendored && len(vendorRoot) < len(bestMatchVendorRoot)) {
   261  				// Current match is worse
   262  			} else {
   263  				// Match is ambiguous
   264  				matchError = fmt.Errorf("multiple rules (%s and %s) may be imported with %q from %s", bestMatch.label, m.label, imp.imp, from)
   265  			}
   266  
   267  		default:
   268  			if bestMatch == nil {
   269  				bestMatch = m
   270  			} else {
   271  				matchError = fmt.Errorf("multiple rules (%s and %s) may be imported with %q from %s", bestMatch.label, m.label, imp.imp, from)
   272  			}
   273  		}
   274  	}
   275  	if matchError != nil {
   276  		return nil, matchError
   277  	}
   278  	if bestMatch == nil {
   279  		return nil, ruleNotFoundError{from, imp.imp}
   280  	}
   281  	if bestMatch.label.Equal(from) {
   282  		return nil, selfImportError{from, imp.imp}
   283  	}
   284  
   285  	if imp.lang == config.ProtoLang && lang == config.GoLang {
   286  		importpath := bestMatch.rule.AttrString("importpath")
   287  		if betterMatch, err := ix.findRuleByImport(importSpec{config.GoLang, importpath}, config.GoLang, from); err == nil {
   288  			return betterMatch, nil
   289  		}
   290  	}
   291  
   292  	return bestMatch, nil
   293  }
   294  
   295  func (ix *RuleIndex) findLabelByImport(imp importSpec, lang config.Language, from label.Label) (label.Label, error) {
   296  	r, err := ix.findRuleByImport(imp, lang, from)
   297  	if err != nil {
   298  		return label.NoLabel, err
   299  	}
   300  	return r.label, nil
   301  }
   302  
   303  func findGoProtoSources(ix *RuleIndex, r *ruleRecord) []importSpec {
   304  	protoLabel, err := label.Parse(r.rule.AttrString("proto"))
   305  	if err != nil {
   306  		return nil
   307  	}
   308  	proto, ok := ix.findRuleByLabel(protoLabel, r.label)
   309  	if !ok {
   310  		return nil
   311  	}
   312  	var importedAs []importSpec
   313  	for _, source := range findSources(proto.rule, proto.label.Pkg, ".proto") {
   314  		importedAs = append(importedAs, importSpec{lang: config.ProtoLang, imp: source})
   315  	}
   316  	return importedAs
   317  }
   318  
   319  func findSources(r bf.Rule, buildRel, ext string) []string {
   320  	srcsExpr := r.Attr("srcs")
   321  	srcsList, ok := srcsExpr.(*bf.ListExpr)
   322  	if !ok {
   323  		return nil
   324  	}
   325  	var srcs []string
   326  	for _, srcExpr := range srcsList.List {
   327  		src, ok := srcExpr.(*bf.StringExpr)
   328  		if !ok {
   329  			continue
   330  		}
   331  		label, err := label.Parse(src.Value)
   332  		if err != nil || !label.Relative || !strings.HasSuffix(label.Name, ext) {
   333  			continue
   334  		}
   335  		srcs = append(srcs, path.Join(buildRel, label.Name))
   336  	}
   337  	return srcs
   338  }
   339  
   340  func isGoLibrary(kind string) bool {
   341  	return kind == "go_library" || isGoProtoLibrary(kind)
   342  }
   343  
   344  func isGoProtoLibrary(kind string) bool {
   345  	return kind == "go_proto_library" || kind == "go_grpc_library"
   346  }