github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/go/resolve.go (about)

     1  /* Copyright 2018 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 golang
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"go/build"
    22  	"log"
    23  	"path"
    24  	"strings"
    25  
    26  	"github.com/bazelbuild/bazel-gazelle/config"
    27  	"github.com/bazelbuild/bazel-gazelle/label"
    28  	"github.com/bazelbuild/bazel-gazelle/pathtools"
    29  	"github.com/bazelbuild/bazel-gazelle/repo"
    30  	"github.com/bazelbuild/bazel-gazelle/resolve"
    31  	"github.com/bazelbuild/bazel-gazelle/rule"
    32  )
    33  
    34  func (*goLang) Imports(_ *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec {
    35  	if !isGoLibrary(r.Kind()) || isExtraLibrary(r) {
    36  		return nil
    37  	}
    38  	if importPath := r.AttrString("importpath"); importPath == "" {
    39  		return []resolve.ImportSpec{}
    40  	} else {
    41  		return []resolve.ImportSpec{{
    42  			Lang: goName,
    43  			Imp:  importPath,
    44  		}}
    45  	}
    46  }
    47  
    48  func (*goLang) Embeds(r *rule.Rule, from label.Label) []label.Label {
    49  	embedStrings := r.AttrStrings("embed")
    50  	if isGoProtoLibrary(r.Kind()) {
    51  		embedStrings = append(embedStrings, r.AttrString("proto"))
    52  	}
    53  	embedLabels := make([]label.Label, 0, len(embedStrings))
    54  	for _, s := range embedStrings {
    55  		l, err := label.Parse(s)
    56  		if err != nil {
    57  			continue
    58  		}
    59  		l = l.Abs(from.Repo, from.Pkg)
    60  		embedLabels = append(embedLabels, l)
    61  	}
    62  	return embedLabels
    63  }
    64  
    65  func (gl *goLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, importsRaw interface{}, from label.Label) {
    66  	if importsRaw == nil {
    67  		// may not be set in tests.
    68  		return
    69  	}
    70  	imports := importsRaw.(rule.PlatformStrings)
    71  	r.DelAttr("deps")
    72  	var resolve func(*config.Config, *resolve.RuleIndex, *repo.RemoteCache, string, label.Label) (label.Label, error)
    73  	switch r.Kind() {
    74  	case "go_proto_library":
    75  		resolve = resolveProto
    76  	default:
    77  		resolve = ResolveGo
    78  	}
    79  	deps, errs := imports.Map(func(imp string) (string, error) {
    80  		l, err := resolve(c, ix, rc, imp, from)
    81  		if err == errSkipImport {
    82  			return "", nil
    83  		} else if err != nil {
    84  			return "", err
    85  		}
    86  		for _, embed := range gl.Embeds(r, from) {
    87  			if embed.Equal(l) {
    88  				return "", nil
    89  			}
    90  		}
    91  		l = l.Rel(from.Repo, from.Pkg)
    92  		return l.String(), nil
    93  	})
    94  	for _, err := range errs {
    95  		log.Print(err)
    96  	}
    97  	if !deps.IsEmpty() {
    98  		if r.Kind() == "go_proto_library" {
    99  			// protos may import the same library multiple times by different names,
   100  			// so we need to de-duplicate them. Protos are not platform-specific,
   101  			// so it's safe to just flatten them.
   102  			r.SetAttr("deps", deps.Flat())
   103  		} else {
   104  			r.SetAttr("deps", deps)
   105  		}
   106  	}
   107  }
   108  
   109  var (
   110  	errSkipImport = errors.New("std or self import")
   111  	errNotFound   = errors.New("rule not found")
   112  )
   113  
   114  // ResolveGo resolves a Go import path to a Bazel label, possibly using the
   115  // given rule index and remote cache. Some special cases may be applied to
   116  // known proto import paths, depending on the current proto mode.
   117  //
   118  // This may be used directly by other language extensions related to Go
   119  // (gomock). Gazelle calls Language.Resolve instead.
   120  func ResolveGo(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, imp string, from label.Label) (label.Label, error) {
   121  	gc := getGoConfig(c)
   122  	if build.IsLocalImport(imp) {
   123  		cleanRel := path.Clean(path.Join(from.Pkg, imp))
   124  		if build.IsLocalImport(cleanRel) {
   125  			return label.NoLabel, fmt.Errorf("relative import path %q from %q points outside of repository", imp, from.Pkg)
   126  		}
   127  		imp = path.Join(gc.prefix, cleanRel)
   128  	}
   129  
   130  	if IsStandard(imp) {
   131  		return label.NoLabel, errSkipImport
   132  	}
   133  
   134  	if l, ok := resolve.FindRuleWithOverride(c, resolve.ImportSpec{Lang: "go", Imp: imp}, "go"); ok {
   135  		return l, nil
   136  	}
   137  
   138  	if l, err := resolveWithIndexGo(c, ix, imp, from); err == nil || err == errSkipImport {
   139  		return l, err
   140  	} else if err != errNotFound {
   141  		return label.NoLabel, err
   142  	}
   143  
   144  	// Special cases for rules_go and bazel_gazelle.
   145  	// These have names that don't following conventions and they're
   146  	// typeically declared with http_archive, not go_repository, so Gazelle
   147  	// won't recognize them.
   148  	if !c.Bzlmod {
   149  		if pathtools.HasPrefix(imp, "github.com/bazelbuild/rules_go") {
   150  			pkg := pathtools.TrimPrefix(imp, "github.com/bazelbuild/rules_go")
   151  			return label.New("io_bazel_rules_go", pkg, "go_default_library"), nil
   152  		} else if pathtools.HasPrefix(imp, "github.com/bazelbuild/bazel-gazelle") {
   153  			pkg := pathtools.TrimPrefix(imp, "github.com/bazelbuild/bazel-gazelle")
   154  			return label.New("bazel_gazelle", pkg, "go_default_library"), nil
   155  		}
   156  	}
   157  
   158  	if !c.IndexLibraries {
   159  		// packages in current repo were not indexed, relying on prefix to decide what may have been in
   160  		// current repo
   161  		if pathtools.HasPrefix(imp, gc.prefix) {
   162  			pkg := path.Join(gc.prefixRel, pathtools.TrimPrefix(imp, gc.prefix))
   163  			libName := libNameByConvention(gc.goNamingConvention, imp, "")
   164  			return label.New("", pkg, libName), nil
   165  		}
   166  	}
   167  
   168  	if gc.depMode == vendorMode {
   169  		return resolveVendored(gc, imp)
   170  	}
   171  	var resolveFn func(string) (string, string, error)
   172  	if gc.depMode == staticMode {
   173  		resolveFn = rc.RootStatic
   174  	} else if gc.moduleMode || pathWithoutSemver(imp) != "" {
   175  		resolveFn = rc.Mod
   176  	} else {
   177  		resolveFn = rc.Root
   178  	}
   179  	return resolveToExternalLabel(c, resolveFn, imp)
   180  }
   181  
   182  // IsStandard returns whether a package is in the standard library.
   183  func IsStandard(imp string) bool {
   184  	return stdPackages[imp]
   185  }
   186  
   187  func resolveWithIndexGo(c *config.Config, ix *resolve.RuleIndex, imp string, from label.Label) (label.Label, error) {
   188  	matches := ix.FindRulesByImportWithConfig(c, resolve.ImportSpec{Lang: "go", Imp: imp}, "go")
   189  	var bestMatch resolve.FindResult
   190  	var bestMatchIsVendored bool
   191  	var bestMatchVendorRoot string
   192  	var bestMatchEmbedsProtos bool
   193  	var matchError error
   194  	goRepositoryMode := getGoConfig(c).goRepositoryMode
   195  
   196  	for _, m := range matches {
   197  		// Apply vendoring logic for Go libraries. A library in a vendor directory
   198  		// is only visible in the parent tree. Vendored libraries supercede
   199  		// non-vendored libraries, and libraries closer to from.Pkg supercede
   200  		// those further up the tree.
   201  		//
   202  		// Also, in external repositories, prefer go_proto_library targets to checked-in .go files
   203  		// pregenerated from .proto files over go_proto_library targets. Ideally, the two should be
   204  		// in sync. If not, users can choose between the two by using the go_generate_proto
   205  		// directive.
   206  		isVendored := false
   207  		vendorRoot := ""
   208  		parts := strings.Split(m.Label.Pkg, "/")
   209  		for i := len(parts) - 1; i >= 0; i-- {
   210  			if parts[i] == "vendor" {
   211  				isVendored = true
   212  				vendorRoot = strings.Join(parts[:i], "/")
   213  				break
   214  			}
   215  		}
   216  		if isVendored && !label.New(m.Label.Repo, vendorRoot, "").Contains(from) {
   217  			// vendor directory not visible
   218  			continue
   219  		}
   220  
   221  		embedsProtos := false
   222  		for _, embed := range m.Embeds {
   223  			if strings.HasSuffix(embed.Name, goProtoSuffix) {
   224  				embedsProtos = true
   225  			}
   226  		}
   227  
   228  		if bestMatch.Label.Equal(label.NoLabel) ||
   229  			(isVendored && (!bestMatchIsVendored || len(vendorRoot) > len(bestMatchVendorRoot))) ||
   230  			(goRepositoryMode && !bestMatchEmbedsProtos && embedsProtos) {
   231  			// Current match is better
   232  			bestMatch = m
   233  			bestMatchIsVendored = isVendored
   234  			bestMatchVendorRoot = vendorRoot
   235  			bestMatchEmbedsProtos = embedsProtos
   236  			matchError = nil
   237  		} else if (!isVendored && bestMatchIsVendored) ||
   238  			(isVendored && len(vendorRoot) < len(bestMatchVendorRoot)) ||
   239  			(goRepositoryMode && bestMatchEmbedsProtos && !embedsProtos) {
   240  			// Current match is worse
   241  		} else {
   242  			// Match is ambiguous
   243  			// TODO: consider listing all the ambiguous rules here.
   244  			matchError = fmt.Errorf("rule %s imports %q which matches multiple rules: %s and %s. # gazelle:resolve may be used to disambiguate", from, imp, bestMatch.Label, m.Label)
   245  		}
   246  	}
   247  	if matchError != nil {
   248  		return label.NoLabel, matchError
   249  	}
   250  	if bestMatch.Label.Equal(label.NoLabel) {
   251  		return label.NoLabel, errNotFound
   252  	}
   253  	if bestMatch.IsSelfImport(from) {
   254  		return label.NoLabel, errSkipImport
   255  	}
   256  	return bestMatch.Label, nil
   257  }
   258  
   259  func resolveToExternalLabel(c *config.Config, resolveFn func(string) (string, string, error), imp string) (label.Label, error) {
   260  	prefix, repo, err := resolveFn(imp)
   261  	if err != nil {
   262  		return label.NoLabel, err
   263  	} else if prefix == "" && repo == "" {
   264  		return label.NoLabel, errSkipImport
   265  	}
   266  
   267  	var pkg string
   268  	if pathtools.HasPrefix(imp, prefix) {
   269  		pkg = pathtools.TrimPrefix(imp, prefix)
   270  	} else if impWithoutSemver := pathWithoutSemver(imp); pathtools.HasPrefix(impWithoutSemver, prefix) {
   271  		// We may have used minimal module compatibility to resolve a path
   272  		// without a semantic import version suffix to a repository that has one.
   273  		pkg = pathtools.TrimPrefix(impWithoutSemver, prefix)
   274  	}
   275  
   276  	// Determine what naming convention is used by the repository.
   277  	// If there is no known repository, it's probably declared in an http_archive
   278  	// somewhere like go_rules_dependencies, so use the old naming convention,
   279  	// unless the user has explicitly told us otherwise.
   280  	// If the repository uses the import_alias convention (default for
   281  	// go_repository), use the convention from the current directory unless the
   282  	// user has told us otherwise.
   283  	gc := getGoConfig(c)
   284  	nc := gc.repoNamingConvention[repo]
   285  	if nc == unknownNamingConvention {
   286  		if gc.goNamingConventionExternal != unknownNamingConvention {
   287  			nc = gc.goNamingConventionExternal
   288  		} else {
   289  			nc = goDefaultLibraryNamingConvention
   290  		}
   291  	} else if nc == importAliasNamingConvention {
   292  		if gc.goNamingConventionExternal != unknownNamingConvention {
   293  			nc = gc.goNamingConventionExternal
   294  		} else {
   295  			nc = gc.goNamingConvention
   296  		}
   297  	}
   298  
   299  	name := libNameByConvention(nc, imp, "")
   300  	return label.New(repo, pkg, name), nil
   301  }
   302  
   303  func resolveVendored(gc *goConfig, imp string) (label.Label, error) {
   304  	name := libNameByConvention(gc.goNamingConvention, imp, "")
   305  	return label.New("", path.Join("vendor", imp), name), nil
   306  }
   307  
   308  func resolveProto(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, imp string, from label.Label) (label.Label, error) {
   309  	if wellKnownProtos[imp] {
   310  		return label.NoLabel, errSkipImport
   311  	}
   312  
   313  	if l, ok := resolve.FindRuleWithOverride(c, resolve.ImportSpec{Lang: "proto", Imp: imp}, "go"); ok {
   314  		return l, nil
   315  	}
   316  
   317  	if l, err := resolveWithIndexProto(c, ix, imp, from); err == nil || err == errSkipImport {
   318  		return l, err
   319  	} else if err != errNotFound {
   320  		return label.NoLabel, err
   321  	}
   322  
   323  	// As a fallback, guess the label based on the proto file name. We assume
   324  	// all proto files in a directory belong to the same package, and the
   325  	// package name matches the directory base name. We also assume that protos
   326  	// in the vendor directory must refer to something else in vendor.
   327  	rel := path.Dir(imp)
   328  	if rel == "." {
   329  		rel = ""
   330  	}
   331  	if from.Pkg == "vendor" || strings.HasPrefix(from.Pkg, "vendor/") {
   332  		rel = path.Join("vendor", rel)
   333  	}
   334  	libName := libNameByConvention(getGoConfig(c).goNamingConvention, imp, "")
   335  	return label.New("", rel, libName), nil
   336  }
   337  
   338  // wellKnownProtos is the set of proto sets for which we don't need to add
   339  // an explicit dependency in go_proto_library.
   340  // TODO(jayconrod): generate from
   341  // @io_bazel_rules_go//proto/wkt:WELL_KNOWN_TYPE_PACKAGES
   342  var wellKnownProtos = map[string]bool{
   343  	"google/protobuf/any.proto":             true,
   344  	"google/protobuf/api.proto":             true,
   345  	"google/protobuf/compiler/plugin.proto": true,
   346  	"google/protobuf/descriptor.proto":      true,
   347  	"google/protobuf/duration.proto":        true,
   348  	"google/protobuf/empty.proto":           true,
   349  	"google/protobuf/field_mask.proto":      true,
   350  	"google/protobuf/source_context.proto":  true,
   351  	"google/protobuf/struct.proto":          true,
   352  	"google/protobuf/timestamp.proto":       true,
   353  	"google/protobuf/type.proto":            true,
   354  	"google/protobuf/wrappers.proto":        true,
   355  }
   356  
   357  func resolveWithIndexProto(c *config.Config, ix *resolve.RuleIndex, imp string, from label.Label) (label.Label, error) {
   358  	matches := ix.FindRulesByImportWithConfig(c, resolve.ImportSpec{Lang: "proto", Imp: imp}, "go")
   359  	if len(matches) == 0 {
   360  		return label.NoLabel, errNotFound
   361  	}
   362  	if len(matches) > 1 {
   363  		return label.NoLabel, fmt.Errorf("multiple rules (%s and %s) may be imported with %q from %s", matches[0].Label, matches[1].Label, imp, from)
   364  	}
   365  	if matches[0].IsSelfImport(from) {
   366  		return label.NoLabel, errSkipImport
   367  	}
   368  	return matches[0].Label, nil
   369  }
   370  
   371  func isGoLibrary(kind string) bool {
   372  	return kind == "go_library" || isGoProtoLibrary(kind)
   373  }
   374  
   375  func isGoProtoLibrary(kind string) bool {
   376  	return kind == "go_proto_library" || kind == "go_grpc_library"
   377  }
   378  
   379  // isExtraLibrary returns true if this rule is one of a handful of proto
   380  // libraries generated by maybeGenerateExtraLib. It should not be indexed for
   381  // dependency resolution.
   382  func isExtraLibrary(r *rule.Rule) bool {
   383  	if !strings.HasSuffix(r.Name(), "_gen") {
   384  		return false
   385  	}
   386  	switch r.AttrString("importpath") {
   387  	case "github.com/golang/protobuf/descriptor",
   388  		"github.com/golang/protobuf/protoc-gen-go/generator",
   389  		"github.com/golang/protobuf/ptypes":
   390  		return true
   391  	default:
   392  		return false
   393  	}
   394  }