kubernetes/vendor/github.com/bazelbuild/bazel-gazelle/internal/resolve/resolve.go

381 lines
11 KiB
Go

/* 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)
}