| /* Copyright 2016 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" |
| "go/build" |
| "log" |
| "path" |
| "strings" |
| |
| "github.com/bazelbuild/bazel-gazelle/internal/config" |
| "github.com/bazelbuild/bazel-gazelle/internal/label" |
| "github.com/bazelbuild/bazel-gazelle/internal/pathtools" |
| "github.com/bazelbuild/bazel-gazelle/internal/repos" |
| bf "github.com/bazelbuild/buildtools/build" |
| ) |
| |
| // Resolver resolves import strings in source files (import paths in Go, |
| // import statements in protos) into Bazel labels. |
| type Resolver struct { |
| c *config.Config |
| l *label.Labeler |
| ix *RuleIndex |
| external nonlocalResolver |
| } |
| |
| // nonlocalResolver resolves import paths outside of the current repository's |
| // prefix. Once we have smarter import path resolution, this shouldn't |
| // be necessary, and we can remove this abstraction. |
| type nonlocalResolver interface { |
| resolve(imp string) (label.Label, error) |
| } |
| |
| func NewResolver(c *config.Config, l *label.Labeler, ix *RuleIndex, rc *repos.RemoteCache) *Resolver { |
| var e nonlocalResolver |
| switch c.DepMode { |
| case config.ExternalMode: |
| e = newExternalResolver(l, rc) |
| case config.VendorMode: |
| e = newVendoredResolver(l) |
| } |
| |
| return &Resolver{ |
| c: c, |
| l: l, |
| ix: ix, |
| external: e, |
| } |
| } |
| |
| // ResolveRule copies and modifies a generated rule e by replacing the import |
| // paths in the "_gazelle_imports" attribute with labels in a "deps" |
| // attribute. This may be safely called on expressions that aren't Go rules |
| // (the original expression will be returned). Any existing "deps" attribute |
| // is deleted, so it may be necessary to merge the result. |
| func (r *Resolver) ResolveRule(e bf.Expr, pkgRel string) bf.Expr { |
| call, ok := e.(*bf.CallExpr) |
| if !ok { |
| return e |
| } |
| rule := bf.Rule{Call: call} |
| from := label.New("", pkgRel, rule.Name()) |
| |
| var resolve func(imp string, from label.Label) (label.Label, error) |
| var embeds []label.Label |
| switch rule.Kind() { |
| case "go_library", "go_binary", "go_test": |
| resolve = r.resolveGo |
| embeds = getEmbedsGo(call, from) |
| case "proto_library": |
| resolve = r.resolveProto |
| case "go_proto_library", "go_grpc_library": |
| resolve = r.resolveGoProto |
| embeds = getEmbedsGo(call, from) |
| default: |
| return e |
| } |
| |
| resolved := *call |
| resolved.List = append([]bf.Expr{}, call.List...) |
| rule.Call = &resolved |
| |
| imports := rule.Attr(config.GazelleImportsKey) |
| rule.DelAttr(config.GazelleImportsKey) |
| rule.DelAttr("deps") |
| deps := mapExprStrings(imports, func(imp string) string { |
| label, err := resolve(imp, from) |
| if err != nil { |
| switch err.(type) { |
| case standardImportError, selfImportError: |
| return "" |
| default: |
| log.Print(err) |
| return "" |
| } |
| } |
| for _, e := range embeds { |
| if label.Equal(e) { |
| return "" |
| } |
| } |
| label.Relative = label.Repo == "" && label.Pkg == pkgRel |
| return label.String() |
| }) |
| if deps != nil { |
| rule.SetAttr("deps", deps) |
| } |
| |
| return &resolved |
| } |
| |
| type standardImportError struct { |
| imp string |
| } |
| |
| func (e standardImportError) Error() string { |
| return fmt.Sprintf("import path %q is in the standard library", e.imp) |
| } |
| |
| // mapExprStrings applies a function f to the strings in e and returns a new |
| // expression with the results. Scalar strings, lists, dicts, selects, and |
| // concatenations are supported. |
| func mapExprStrings(e bf.Expr, f func(string) string) bf.Expr { |
| if e == nil { |
| return nil |
| } |
| switch expr := e.(type) { |
| case *bf.StringExpr: |
| s := f(expr.Value) |
| if s == "" { |
| return nil |
| } |
| ret := *expr |
| ret.Value = s |
| return &ret |
| |
| case *bf.ListExpr: |
| var list []bf.Expr |
| for _, elem := range expr.List { |
| elem = mapExprStrings(elem, f) |
| if elem != nil { |
| list = append(list, elem) |
| } |
| } |
| if len(list) == 0 && len(expr.List) > 0 { |
| return nil |
| } |
| ret := *expr |
| ret.List = list |
| return &ret |
| |
| case *bf.DictExpr: |
| var cases []bf.Expr |
| isEmpty := true |
| for _, kv := range expr.List { |
| keyval, ok := kv.(*bf.KeyValueExpr) |
| if !ok { |
| log.Panicf("unexpected expression in generated imports dict: %#v", kv) |
| } |
| value := mapExprStrings(keyval.Value, f) |
| if value != nil { |
| cases = append(cases, &bf.KeyValueExpr{Key: keyval.Key, Value: value}) |
| if key, ok := keyval.Key.(*bf.StringExpr); !ok || key.Value != "//conditions:default" { |
| isEmpty = false |
| } |
| } |
| } |
| if isEmpty { |
| return nil |
| } |
| ret := *expr |
| ret.List = cases |
| return &ret |
| |
| case *bf.CallExpr: |
| if x, ok := expr.X.(*bf.LiteralExpr); !ok || x.Token != "select" || len(expr.List) != 1 { |
| log.Panicf("unexpected call expression in generated imports: %#v", e) |
| } |
| arg := mapExprStrings(expr.List[0], f) |
| if arg == nil { |
| return nil |
| } |
| call := *expr |
| call.List[0] = arg |
| return &call |
| |
| case *bf.BinaryExpr: |
| x := mapExprStrings(expr.X, f) |
| y := mapExprStrings(expr.Y, f) |
| if x == nil { |
| return y |
| } |
| if y == nil { |
| return x |
| } |
| binop := *expr |
| binop.X = x |
| binop.Y = y |
| return &binop |
| |
| default: |
| log.Panicf("unexpected expression in generated imports: %#v", e) |
| return nil |
| } |
| } |
| |
| // resolveGo resolves an import path from a Go source file to a label. |
| // pkgRel is the path to the Go package relative to the repository root; it |
| // is used to resolve relative imports. |
| func (r *Resolver) resolveGo(imp string, from label.Label) (label.Label, error) { |
| if build.IsLocalImport(imp) { |
| cleanRel := path.Clean(path.Join(from.Pkg, imp)) |
| if build.IsLocalImport(cleanRel) { |
| return label.NoLabel, fmt.Errorf("relative import path %q from %q points outside of repository", imp, from.Pkg) |
| } |
| imp = path.Join(r.c.GoPrefix, cleanRel) |
| } |
| |
| if IsStandard(imp) { |
| return label.NoLabel, standardImportError{imp} |
| } |
| |
| if l := resolveWellKnownGo(imp); !l.Equal(label.NoLabel) { |
| return l, nil |
| } |
| |
| if l, err := r.ix.findLabelByImport(importSpec{config.GoLang, imp}, config.GoLang, from); err != nil { |
| if _, ok := err.(ruleNotFoundError); !ok { |
| return label.NoLabel, err |
| } |
| } else { |
| return l, nil |
| } |
| |
| if pathtools.HasPrefix(imp, r.c.GoPrefix) { |
| return r.l.LibraryLabel(pathtools.TrimPrefix(imp, r.c.GoPrefix)), nil |
| } |
| |
| return r.external.resolve(imp) |
| } |
| |
| // resolveProto resolves an import statement in a .proto file to a label |
| // for a proto_library rule. |
| func (r *Resolver) resolveProto(imp string, from label.Label) (label.Label, error) { |
| if !strings.HasSuffix(imp, ".proto") { |
| return label.NoLabel, fmt.Errorf("can't import non-proto: %q", imp) |
| } |
| if isWellKnownProto(imp) { |
| name := path.Base(imp[:len(imp)-len(".proto")]) + "_proto" |
| return label.New(config.WellKnownTypesProtoRepo, "", name), nil |
| } |
| |
| if l, err := r.ix.findLabelByImport(importSpec{config.ProtoLang, imp}, config.ProtoLang, from); err != nil { |
| if _, ok := err.(ruleNotFoundError); !ok { |
| return label.NoLabel, err |
| } |
| } else { |
| return l, nil |
| } |
| |
| rel := path.Dir(imp) |
| if rel == "." { |
| rel = "" |
| } |
| name := pathtools.RelBaseName(rel, r.c.GoPrefix, r.c.RepoRoot) |
| return r.l.ProtoLabel(rel, name), nil |
| } |
| |
| // resolveGoProto resolves an import statement in a .proto file to a |
| // label for a go_library rule that embeds the corresponding go_proto_library. |
| func (r *Resolver) resolveGoProto(imp string, from label.Label) (label.Label, error) { |
| if !strings.HasSuffix(imp, ".proto") { |
| return label.NoLabel, fmt.Errorf("can't import non-proto: %q", imp) |
| } |
| stem := imp[:len(imp)-len(".proto")] |
| |
| if isWellKnownProto(stem) { |
| return label.NoLabel, standardImportError{imp} |
| } |
| |
| if l, err := r.ix.findLabelByImport(importSpec{config.ProtoLang, imp}, config.GoLang, from); err != nil { |
| if _, ok := err.(ruleNotFoundError); !ok { |
| return label.NoLabel, err |
| } |
| } else { |
| return l, err |
| } |
| |
| // As a fallback, guess the label based on the proto file name. We assume |
| // all proto files in a directory belong to the same package, and the |
| // package name matches the directory base name. We also assume that protos |
| // in the vendor directory must refer to something else in vendor. |
| rel := path.Dir(imp) |
| if rel == "." { |
| rel = "" |
| } |
| if from.Pkg == "vendor" || strings.HasPrefix(from.Pkg, "vendor/") { |
| rel = path.Join("vendor", rel) |
| } |
| return r.l.LibraryLabel(rel), nil |
| } |
| |
| func getEmbedsGo(call *bf.CallExpr, from label.Label) []label.Label { |
| rule := bf.Rule{Call: call} |
| embedStrings := rule.AttrStrings("embed") |
| embedLabels := make([]label.Label, 0, len(embedStrings)) |
| for _, s := range embedStrings { |
| l, err := label.Parse(s) |
| if err != nil { |
| continue |
| } |
| l = l.Abs(from.Repo, from.Pkg) |
| embedLabels = append(embedLabels, l) |
| } |
| return embedLabels |
| } |
| |
| // IsStandard returns whether a package is in the standard library. |
| func IsStandard(imp string) bool { |
| return stdPackages[imp] |
| } |
| |
| func isWellKnownProto(imp string) bool { |
| return pathtools.HasPrefix(imp, config.WellKnownTypesProtoPrefix) && pathtools.TrimPrefix(imp, config.WellKnownTypesProtoPrefix) == path.Base(imp) |
| } |
| |
| func resolveWellKnownGo(imp string) label.Label { |
| // keep in sync with @io_bazel_rules_go//proto/wkt:well_known_types.bzl |
| // TODO(jayconrod): in well_known_types.bzl, write the import paths and |
| // targets in a public dict. Import it here, and use it to generate this code. |
| switch imp { |
| case "github.com/golang/protobuf/ptypes/any", |
| "github.com/golang/protobuf/ptypes/api", |
| "github.com/golang/protobuf/protoc-gen-go/descriptor", |
| "github.com/golang/protobuf/ptypes/duration", |
| "github.com/golang/protobuf/ptypes/empty", |
| "google.golang.org/genproto/protobuf/field_mask", |
| "google.golang.org/genproto/protobuf/source_context", |
| "github.com/golang/protobuf/ptypes/struct", |
| "github.com/golang/protobuf/ptypes/timestamp", |
| "github.com/golang/protobuf/ptypes/wrappers": |
| return label.Label{ |
| Repo: config.RulesGoRepoName, |
| Pkg: config.WellKnownTypesPkg, |
| Name: path.Base(imp) + "_go_proto", |
| } |
| case "github.com/golang/protobuf/protoc-gen-go/plugin": |
| return label.Label{ |
| Repo: config.RulesGoRepoName, |
| Pkg: config.WellKnownTypesPkg, |
| Name: "compiler_plugin_go_proto", |
| } |
| case "google.golang.org/genproto/protobuf/ptype": |
| return label.Label{ |
| Repo: config.RulesGoRepoName, |
| Pkg: config.WellKnownTypesPkg, |
| Name: "type_go_proto", |
| } |
| } |
| return label.NoLabel |
| } |
| |
| func isWellKnownGo(imp string) bool { |
| prefix := config.WellKnownTypesGoPrefix + "/ptypes/" |
| return strings.HasPrefix(imp, prefix) && strings.TrimPrefix(imp, prefix) == path.Base(imp) |
| } |