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 }