blob: d79fc57cb979f469a0622fef8849c7a87db1269e [file] [log] [blame] [edit]
/* Copyright 2017 The Bazel Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resolve
import (
"fmt"
"log"
"path"
"path/filepath"
"strings"
"github.com/bazelbuild/bazel-gazelle/internal/config"
"github.com/bazelbuild/bazel-gazelle/internal/label"
bf "github.com/bazelbuild/buildtools/build"
)
// RuleIndex is a table of rules in a workspace, indexed by label and by
// import path. Used by Resolver to map import paths to labels.
type RuleIndex struct {
rules []*ruleRecord
labelMap map[label.Label]*ruleRecord
importMap map[importSpec][]*ruleRecord
}
// ruleRecord contains information about a rule relevant to import indexing.
type ruleRecord struct {
rule bf.Rule
label label.Label
lang config.Language
importedAs []importSpec
embedded bool
}
// importSpec describes a package to be imported. Language is specified, since
// different languages have different formats for their imports.
type importSpec struct {
lang config.Language
imp string
}
func NewRuleIndex() *RuleIndex {
return &RuleIndex{
labelMap: make(map[label.Label]*ruleRecord),
}
}
// AddRulesFromFile adds existing rules to the index from file
// (which must not be nil).
func (ix *RuleIndex) AddRulesFromFile(c *config.Config, file *bf.File) {
buildRel, err := filepath.Rel(c.RepoRoot, file.Path)
if err != nil {
log.Panicf("file not in repo: %s", file.Path)
}
buildRel = path.Dir(filepath.ToSlash(buildRel))
if buildRel == "." || buildRel == "/" {
buildRel = ""
}
for _, stmt := range file.Stmt {
if call, ok := stmt.(*bf.CallExpr); ok {
ix.addRule(call, c.GoPrefix, buildRel)
}
}
}
func (ix *RuleIndex) addRule(call *bf.CallExpr, goPrefix, buildRel string) {
rule := bf.Rule{Call: call}
record := &ruleRecord{
rule: rule,
label: label.New("", buildRel, rule.Name()),
}
if _, ok := ix.labelMap[record.label]; ok {
log.Printf("multiple rules found with label %s", record.label)
return
}
kind := rule.Kind()
switch {
case isGoLibrary(kind):
record.lang = config.GoLang
if imp := rule.AttrString("importpath"); imp != "" {
record.importedAs = []importSpec{{lang: config.GoLang, imp: imp}}
}
// Additional proto imports may be added in Finish.
case kind == "proto_library":
record.lang = config.ProtoLang
for _, s := range findSources(rule, buildRel, ".proto") {
record.importedAs = append(record.importedAs, importSpec{lang: config.ProtoLang, imp: s})
}
default:
return
}
ix.rules = append(ix.rules, record)
ix.labelMap[record.label] = record
}
// Finish constructs the import index and performs any other necessary indexing
// actions after all rules have been added. This step is necessary because
// a rule may be indexed differently based on what rules are added later.
//
// This function must be called after all AddRulesFromFile calls but before any
// findRuleByImport calls.
func (ix *RuleIndex) Finish() {
ix.skipGoEmbds()
ix.buildImportIndex()
}
// skipGoEmbeds sets the embedded flag on Go library rules that are imported
// by other Go library rules with the same import path. Note that embedded
// rules may still be imported with non-Go imports. For example, a
// go_proto_library may be imported with either a Go import path or a proto
// path. If the library is embedded, only the proto path will be indexed.
func (ix *RuleIndex) skipGoEmbds() {
for _, r := range ix.rules {
if !isGoLibrary(r.rule.Kind()) {
continue
}
importpath := r.rule.AttrString("importpath")
var embedLabels []label.Label
if embedList, ok := r.rule.Attr("embed").(*bf.ListExpr); ok {
for _, embedElem := range embedList.List {
embedStr, ok := embedElem.(*bf.StringExpr)
if !ok {
continue
}
embedLabel, err := label.Parse(embedStr.Value)
if err != nil {
continue
}
embedLabels = append(embedLabels, embedLabel)
}
}
if libraryStr, ok := r.rule.Attr("library").(*bf.StringExpr); ok {
if libraryLabel, err := label.Parse(libraryStr.Value); err == nil {
embedLabels = append(embedLabels, libraryLabel)
}
}
for _, l := range embedLabels {
embed, ok := ix.findRuleByLabel(l, r.label)
if !ok {
continue
}
if embed.rule.AttrString("importpath") != importpath {
continue
}
embed.embedded = true
}
}
}
// buildImportIndex constructs the map used by findRuleByImport.
func (ix *RuleIndex) buildImportIndex() {
ix.importMap = make(map[importSpec][]*ruleRecord)
for _, r := range ix.rules {
if isGoProtoLibrary(r.rule.Kind()) {
protoImports := findGoProtoSources(ix, r)
r.importedAs = append(r.importedAs, protoImports...)
}
for _, imp := range r.importedAs {
if imp.lang == config.GoLang && r.embedded {
continue
}
ix.importMap[imp] = append(ix.importMap[imp], r)
}
}
}
type ruleNotFoundError struct {
from label.Label
imp string
}
func (e ruleNotFoundError) Error() string {
return fmt.Sprintf("no rule found for import %q, needed in %s", e.imp, e.from)
}
type selfImportError struct {
from label.Label
imp string
}
func (e selfImportError) Error() string {
return fmt.Sprintf("rule %s imports itself with path %q", e.from, e.imp)
}
func (ix *RuleIndex) findRuleByLabel(label label.Label, from label.Label) (*ruleRecord, bool) {
label = label.Abs(from.Repo, from.Pkg)
r, ok := ix.labelMap[label]
return r, ok
}
// findRuleByImport attempts to resolve an import string to a rule record.
// imp is the import to resolve (which includes the target language). lang is
// the language of the rule with the dependency (for example, in
// go_proto_library, imp will have ProtoLang and lang will be GoLang).
// from is the rule which is doing the dependency. This is used to check
// vendoring visibility and to check for self-imports.
//
// Any number of rules may provide the same import. If no rules provide the
// import, ruleNotFoundError is returned. If a rule imports itself,
// selfImportError is returned. If multiple rules provide the import, this
// function will attempt to choose one based on Go vendoring logic. In
// ambiguous cases, an error is returned.
func (ix *RuleIndex) findRuleByImport(imp importSpec, lang config.Language, from label.Label) (*ruleRecord, error) {
matches := ix.importMap[imp]
var bestMatch *ruleRecord
var bestMatchIsVendored bool
var bestMatchVendorRoot string
var matchError error
for _, m := range matches {
if m.lang != lang {
continue
}
switch imp.lang {
case config.GoLang:
// Apply vendoring logic for Go libraries. A library in a vendor directory
// is only visible in the parent tree. Vendored libraries supercede
// non-vendored libraries, and libraries closer to from.Pkg supercede
// those further up the tree.
isVendored := false
vendorRoot := ""
parts := strings.Split(m.label.Pkg, "/")
for i := len(parts) - 1; i >= 0; i-- {
if parts[i] == "vendor" {
isVendored = true
vendorRoot = strings.Join(parts[:i], "/")
break
}
}
if isVendored && !label.New(m.label.Repo, vendorRoot, "").Contains(from) {
// vendor directory not visible
continue
}
if bestMatch == nil || isVendored && (!bestMatchIsVendored || len(vendorRoot) > len(bestMatchVendorRoot)) {
// Current match is better
bestMatch = m
bestMatchIsVendored = isVendored
bestMatchVendorRoot = vendorRoot
matchError = nil
} else if (!isVendored && bestMatchIsVendored) || (isVendored && len(vendorRoot) < len(bestMatchVendorRoot)) {
// Current match is worse
} else {
// Match is ambiguous
matchError = fmt.Errorf("multiple rules (%s and %s) may be imported with %q from %s", bestMatch.label, m.label, imp.imp, from)
}
default:
if bestMatch == nil {
bestMatch = m
} else {
matchError = fmt.Errorf("multiple rules (%s and %s) may be imported with %q from %s", bestMatch.label, m.label, imp.imp, from)
}
}
}
if matchError != nil {
return nil, matchError
}
if bestMatch == nil {
return nil, ruleNotFoundError{from, imp.imp}
}
if bestMatch.label.Equal(from) {
return nil, selfImportError{from, imp.imp}
}
if imp.lang == config.ProtoLang && lang == config.GoLang {
importpath := bestMatch.rule.AttrString("importpath")
if betterMatch, err := ix.findRuleByImport(importSpec{config.GoLang, importpath}, config.GoLang, from); err == nil {
return betterMatch, nil
}
}
return bestMatch, nil
}
func (ix *RuleIndex) findLabelByImport(imp importSpec, lang config.Language, from label.Label) (label.Label, error) {
r, err := ix.findRuleByImport(imp, lang, from)
if err != nil {
return label.NoLabel, err
}
return r.label, nil
}
func findGoProtoSources(ix *RuleIndex, r *ruleRecord) []importSpec {
protoLabel, err := label.Parse(r.rule.AttrString("proto"))
if err != nil {
return nil
}
proto, ok := ix.findRuleByLabel(protoLabel, r.label)
if !ok {
return nil
}
var importedAs []importSpec
for _, source := range findSources(proto.rule, proto.label.Pkg, ".proto") {
importedAs = append(importedAs, importSpec{lang: config.ProtoLang, imp: source})
}
return importedAs
}
func findSources(r bf.Rule, buildRel, ext string) []string {
srcsExpr := r.Attr("srcs")
srcsList, ok := srcsExpr.(*bf.ListExpr)
if !ok {
return nil
}
var srcs []string
for _, srcExpr := range srcsList.List {
src, ok := srcExpr.(*bf.StringExpr)
if !ok {
continue
}
label, err := label.Parse(src.Value)
if err != nil || !label.Relative || !strings.HasSuffix(label.Name, ext) {
continue
}
srcs = append(srcs, path.Join(buildRel, label.Name))
}
return srcs
}
func isGoLibrary(kind string) bool {
return kind == "go_library" || isGoProtoLibrary(kind)
}
func isGoProtoLibrary(kind string) bool {
return kind == "go_proto_library" || kind == "go_grpc_library"
}