github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/go/package.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 golang
    17  
    18  import (
    19  	"fmt"
    20  	"log"
    21  	"path"
    22  	"regexp"
    23  	"sort"
    24  	"strings"
    25  
    26  	"github.com/bazelbuild/bazel-gazelle/config"
    27  	"github.com/bazelbuild/bazel-gazelle/language/proto"
    28  	"github.com/bazelbuild/bazel-gazelle/pathtools"
    29  	"github.com/bazelbuild/bazel-gazelle/rule"
    30  )
    31  
    32  // goPackage contains metadata for a set of .go and .proto files that can be
    33  // used to generate Go rules.
    34  type goPackage struct {
    35  	name, dir, rel        string
    36  	library, binary, test goTarget
    37  	tests                 []goTarget
    38  	proto                 protoTarget
    39  	hasTestdata           bool
    40  	hasMainFunction       bool
    41  	importPath            string
    42  }
    43  
    44  // goTarget contains information used to generate an individual Go rule
    45  // (library, binary, or test).
    46  type goTarget struct {
    47  	sources, embedSrcs, imports, cppopts, copts, cxxopts, clinkopts platformStringsBuilder
    48  	cgo, hasInternalTest                                            bool
    49  }
    50  
    51  // protoTarget contains information used to generate a go_proto_library rule.
    52  type protoTarget struct {
    53  	name        string
    54  	sources     platformStringsBuilder
    55  	imports     platformStringsBuilder
    56  	hasServices bool
    57  }
    58  
    59  // platformStringsBuilder is used to construct rule.PlatformStrings. Bazel
    60  // has some requirements for deps list (a dependency cannot appear in more
    61  // than one select expression; dependencies cannot be duplicated), so we need
    62  // to build these carefully.
    63  type platformStringsBuilder struct {
    64  	strs map[string]platformStringInfo
    65  }
    66  
    67  // platformStringInfo contains information about a single string (source,
    68  // import, or option).
    69  type platformStringInfo struct {
    70  	set                 platformStringSet
    71  	osConstraints       map[string]bool
    72  	archConstraints     map[string]bool
    73  	platformConstraints map[rule.PlatformConstraint]bool
    74  }
    75  
    76  type platformStringSet int
    77  
    78  const (
    79  	genericSet platformStringSet = iota
    80  	osSet
    81  	archSet
    82  	platformSet
    83  )
    84  
    85  // Matches a package version, eg. the end segment of 'example.com/foo/v1'
    86  var pkgVersionRe = regexp.MustCompile("^v[0-9]+$")
    87  
    88  // addFile adds the file described by "info" to a target in the package "p" if
    89  // the file is buildable.
    90  //
    91  // "cgo" tells whether any ".go" file in the package contains cgo code. This
    92  // affects whether C files are added to targets.
    93  //
    94  // An error is returned if a file is buildable but invalid (for example, a
    95  // test .go file containing cgo code). Files that are not buildable will not
    96  // be added to any target (for example, .txt files).
    97  func (pkg *goPackage) addFile(c *config.Config, er *embedResolver, info fileInfo, cgo bool) error {
    98  	switch {
    99  	case info.ext == unknownExt || !cgo && (info.ext == cExt || info.ext == csExt):
   100  		return nil
   101  	case info.ext == protoExt:
   102  		if pcMode := getProtoMode(c); pcMode == proto.LegacyMode {
   103  			// Only add files in legacy mode. This is used to generate a filegroup
   104  			// that contains all protos. In order modes, we get the .proto files
   105  			// from information emitted by the proto language extension.
   106  			pkg.proto.addFile(info)
   107  		}
   108  	case info.isTest:
   109  		if info.isCgo {
   110  			return fmt.Errorf("%s: use of cgo in test not supported", info.path)
   111  		}
   112  		if getGoConfig(c).testMode == fileTestMode || len(pkg.tests) == 0 {
   113  			pkg.tests = append(pkg.tests, goTarget{})
   114  		}
   115  		// Add the the file to the most recently added test target (in fileTestMode)
   116  		// or the only test target (in defaultMode).
   117  		// In both cases, this will be the last element in the slice.
   118  		test := &pkg.tests[len(pkg.tests)-1]
   119  		test.addFile(c, er, info)
   120  		if !info.isExternalTest {
   121  			test.hasInternalTest = true
   122  		}
   123  	default:
   124  		pkg.hasMainFunction = pkg.hasMainFunction || info.hasMainFunction
   125  		pkg.library.addFile(c, er, info)
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  // isCommand returns true if the package name is "main".
   132  func (pkg *goPackage) isCommand() bool {
   133  	return pkg.name == "main" && pkg.hasMainFunction
   134  }
   135  
   136  // isBuildable returns true if anything in the package is buildable.
   137  // This is true if the package has Go code that satisfies build constraints
   138  // on any platform or has proto files not in legacy mode.
   139  func (pkg *goPackage) isBuildable(c *config.Config) bool {
   140  	return pkg.firstGoFile() != "" || !pkg.proto.sources.isEmpty()
   141  }
   142  
   143  // firstGoFile returns the name of a .go file if the package contains at least
   144  // one .go file, or "" otherwise.
   145  func (pkg *goPackage) firstGoFile() string {
   146  	goSrcs := []platformStringsBuilder{
   147  		pkg.library.sources,
   148  		pkg.binary.sources,
   149  	}
   150  	for _, test := range pkg.tests {
   151  		goSrcs = append(goSrcs, test.sources)
   152  	}
   153  
   154  	for _, sb := range goSrcs {
   155  		if sb.strs != nil {
   156  			for s := range sb.strs {
   157  				if strings.HasSuffix(s, ".go") {
   158  					return s
   159  				}
   160  			}
   161  		}
   162  	}
   163  	return ""
   164  }
   165  
   166  func (pkg *goPackage) haveCgo() bool {
   167  	if pkg.library.cgo || pkg.binary.cgo {
   168  		return true
   169  	}
   170  	for _, t := range pkg.tests {
   171  		if t.cgo {
   172  			return true
   173  		}
   174  	}
   175  	return false
   176  }
   177  
   178  func (pkg *goPackage) inferImportPath(c *config.Config) error {
   179  	if pkg.importPath != "" {
   180  		log.Panic("importPath already set")
   181  	}
   182  	gc := getGoConfig(c)
   183  	if !gc.prefixSet {
   184  		return fmt.Errorf("%s: go prefix is not set, so importpath can't be determined for rules. Set a prefix with a '# gazelle:prefix' comment or with -go_prefix on the command line", pkg.dir)
   185  	}
   186  	pkg.importPath = InferImportPath(c, pkg.rel)
   187  	return nil
   188  }
   189  
   190  // libNameFromImportPath returns a a suitable go_library name based on the import path.
   191  // Major version suffixes (eg. "v1") are dropped.
   192  func libNameFromImportPath(dir string) string {
   193  	i := strings.LastIndexAny(dir, "/\\")
   194  	if i < 0 {
   195  		return dir
   196  	}
   197  	name := dir[i+1:]
   198  	if pkgVersionRe.MatchString(name) {
   199  		dir := dir[:i]
   200  		i = strings.LastIndexAny(dir, "/\\")
   201  		if i >= 0 {
   202  			name = dir[i+1:]
   203  		}
   204  	}
   205  	return strings.ReplaceAll(name, ".", "_")
   206  }
   207  
   208  // libNameByConvention returns a suitable name for a go_library using the given
   209  // naming convention, the import path, and the package name.
   210  func libNameByConvention(nc namingConvention, imp string, pkgName string) string {
   211  	if nc == goDefaultLibraryNamingConvention {
   212  		return defaultLibName
   213  	}
   214  	name := libNameFromImportPath(imp)
   215  	isCommand := pkgName == "main"
   216  	if name == "" {
   217  		if isCommand {
   218  			name = "lib"
   219  		} else {
   220  			name = pkgName
   221  		}
   222  	} else if isCommand {
   223  		name += "_lib"
   224  	}
   225  	return name
   226  }
   227  
   228  // testNameByConvention returns a suitable name for a go_test using the given
   229  // naming convention and the import path.
   230  func testNameByConvention(nc namingConvention, imp string) string {
   231  	if nc == goDefaultLibraryNamingConvention {
   232  		return defaultTestName
   233  	}
   234  	libName := libNameFromImportPath(imp)
   235  	if libName == "" {
   236  		libName = "lib"
   237  	}
   238  	return libName + "_test"
   239  }
   240  
   241  // testNameFromSingleSource returns a suitable name for a go_test using the
   242  // single Go source file name.
   243  func testNameFromSingleSource(src string) string {
   244  	if i := strings.LastIndexByte(src, '.'); i >= 0 {
   245  		src = src[0:i]
   246  	}
   247  	libName := libNameFromImportPath(src)
   248  	if libName == "" {
   249  		return ""
   250  	}
   251  	if strings.HasSuffix(libName, "_test") {
   252  		return libName
   253  	}
   254  	return libName + "_test"
   255  }
   256  
   257  // binName returns a suitable name for a go_binary.
   258  func binName(rel, prefix, repoRoot string) string {
   259  	return pathtools.RelBaseName(rel, prefix, repoRoot)
   260  }
   261  
   262  func InferImportPath(c *config.Config, rel string) string {
   263  	gc := getGoConfig(c)
   264  	if rel == gc.prefixRel {
   265  		return gc.prefix
   266  	} else {
   267  		fromPrefixRel := strings.TrimPrefix(rel, gc.prefixRel+"/")
   268  		return path.Join(gc.prefix, fromPrefixRel)
   269  	}
   270  }
   271  
   272  func goProtoPackageName(pkg proto.Package) string {
   273  	if value, ok := pkg.Options["go_package"]; ok {
   274  		if strings.LastIndexByte(value, '/') == -1 {
   275  			return value
   276  		} else {
   277  			if i := strings.LastIndexByte(value, ';'); i != -1 {
   278  				return value[i+1:]
   279  			} else {
   280  				return path.Base(value)
   281  			}
   282  		}
   283  	}
   284  	return strings.Replace(pkg.Name, ".", "_", -1)
   285  }
   286  
   287  func goProtoImportPath(c *config.Config, pkg proto.Package, rel string) string {
   288  	if value, ok := pkg.Options["go_package"]; ok {
   289  		if strings.LastIndexByte(value, '/') == -1 {
   290  			return InferImportPath(c, rel)
   291  		} else if i := strings.LastIndexByte(value, ';'); i != -1 {
   292  			return value[:i]
   293  		} else {
   294  			return value
   295  		}
   296  	}
   297  	return InferImportPath(c, rel)
   298  }
   299  
   300  func (t *goTarget) addFile(c *config.Config, er *embedResolver, info fileInfo) {
   301  	t.cgo = t.cgo || info.isCgo
   302  	add := getPlatformStringsAddFunction(c, info, nil)
   303  	add(&t.sources, info.name)
   304  	add(&t.imports, info.imports...)
   305  	if er != nil {
   306  		for _, embed := range info.embeds {
   307  			embedSrcs, err := er.resolve(embed)
   308  			if err != nil {
   309  				log.Print(err)
   310  				continue
   311  			}
   312  			add(&t.embedSrcs, embedSrcs...)
   313  		}
   314  	}
   315  	for _, cppopts := range info.cppopts {
   316  		optAdd := add
   317  		if !cppopts.empty() {
   318  			optAdd = getPlatformStringsAddFunction(c, info, cppopts)
   319  		}
   320  		optAdd(&t.cppopts, cppopts.opts)
   321  	}
   322  	for _, copts := range info.copts {
   323  		optAdd := add
   324  		if !copts.empty() {
   325  			optAdd = getPlatformStringsAddFunction(c, info, copts)
   326  		}
   327  		optAdd(&t.copts, copts.opts)
   328  	}
   329  	for _, cxxopts := range info.cxxopts {
   330  		optAdd := add
   331  		if !cxxopts.empty() {
   332  			optAdd = getPlatformStringsAddFunction(c, info, cxxopts)
   333  		}
   334  		optAdd(&t.cxxopts, cxxopts.opts)
   335  	}
   336  	for _, clinkopts := range info.clinkopts {
   337  		optAdd := add
   338  		if !clinkopts.empty() {
   339  			optAdd = getPlatformStringsAddFunction(c, info, clinkopts)
   340  		}
   341  		optAdd(&t.clinkopts, clinkopts.opts)
   342  	}
   343  }
   344  
   345  func protoTargetFromProtoPackage(name string, pkg proto.Package) protoTarget {
   346  	target := protoTarget{name: name}
   347  	for f := range pkg.Files {
   348  		target.sources.addGenericString(f)
   349  	}
   350  	for i := range pkg.Imports {
   351  		target.imports.addGenericString(i)
   352  	}
   353  	target.hasServices = pkg.HasServices
   354  	return target
   355  }
   356  
   357  func (t *protoTarget) addFile(info fileInfo) {
   358  	t.sources.addGenericString(info.name)
   359  	for _, imp := range info.imports {
   360  		t.imports.addGenericString(imp)
   361  	}
   362  	t.hasServices = t.hasServices || info.hasServices
   363  }
   364  
   365  // getPlatformStringsAddFunction returns a function used to add strings to
   366  // a *platformStringsBuilder under the same set of constraints. This is a
   367  // performance optimization to avoid evaluating constraints repeatedly.
   368  func getPlatformStringsAddFunction(c *config.Config, info fileInfo, cgoTags *cgoTagsAndOpts) func(sb *platformStringsBuilder, ss ...string) {
   369  	isOSSpecific, isArchSpecific := isOSArchSpecific(info, cgoTags)
   370  	v := getGoConfig(c).rulesGoVersion
   371  	constraintPrefix := "@" + getGoConfig(c).rulesGoRepoName + "//go/platform:"
   372  
   373  	switch {
   374  	case !isOSSpecific && !isArchSpecific:
   375  		if checkConstraints(c, "", "", info.goos, info.goarch, info.tags, cgoTags) {
   376  			return func(sb *platformStringsBuilder, ss ...string) {
   377  				for _, s := range ss {
   378  					sb.addGenericString(s)
   379  				}
   380  			}
   381  		}
   382  
   383  	case isOSSpecific && !isArchSpecific:
   384  		var osMatch []string
   385  		for _, os := range rule.KnownOSs {
   386  			if rulesGoSupportsOS(v, os) &&
   387  				checkConstraints(c, os, "", info.goos, info.goarch, info.tags, cgoTags) {
   388  				osMatch = append(osMatch, os)
   389  			}
   390  		}
   391  		if len(osMatch) > 0 {
   392  			return func(sb *platformStringsBuilder, ss ...string) {
   393  				for _, s := range ss {
   394  					sb.addOSString(s, osMatch, constraintPrefix)
   395  				}
   396  			}
   397  		}
   398  
   399  	case !isOSSpecific && isArchSpecific:
   400  		var archMatch []string
   401  		for _, arch := range rule.KnownArchs {
   402  			if rulesGoSupportsArch(v, arch) &&
   403  				checkConstraints(c, "", arch, info.goos, info.goarch, info.tags, cgoTags) {
   404  				archMatch = append(archMatch, arch)
   405  			}
   406  		}
   407  		if len(archMatch) > 0 {
   408  			return func(sb *platformStringsBuilder, ss ...string) {
   409  				for _, s := range ss {
   410  					sb.addArchString(s, archMatch, constraintPrefix)
   411  				}
   412  			}
   413  		}
   414  
   415  	default:
   416  		var platformMatch []rule.Platform
   417  		for _, platform := range rule.KnownPlatforms {
   418  			if rulesGoSupportsPlatform(v, platform) &&
   419  				checkConstraints(c, platform.OS, platform.Arch, info.goos, info.goarch, info.tags, cgoTags) {
   420  				platformMatch = append(platformMatch, platform)
   421  			}
   422  		}
   423  		if len(platformMatch) > 0 {
   424  			return func(sb *platformStringsBuilder, ss ...string) {
   425  				for _, s := range ss {
   426  					sb.addPlatformString(s, platformMatch, constraintPrefix)
   427  				}
   428  			}
   429  		}
   430  	}
   431  
   432  	return func(_ *platformStringsBuilder, _ ...string) {}
   433  }
   434  
   435  func (sb *platformStringsBuilder) isEmpty() bool {
   436  	return sb.strs == nil
   437  }
   438  
   439  func (sb *platformStringsBuilder) hasGo() bool {
   440  	for s := range sb.strs {
   441  		if strings.HasSuffix(s, ".go") {
   442  			return true
   443  		}
   444  	}
   445  	return false
   446  }
   447  
   448  func (sb *platformStringsBuilder) addGenericString(s string) {
   449  	if sb.strs == nil {
   450  		sb.strs = make(map[string]platformStringInfo)
   451  	}
   452  	sb.strs[s] = platformStringInfo{set: genericSet}
   453  }
   454  
   455  func (sb *platformStringsBuilder) addOSString(s string, oss []string, constraintPrefix string) {
   456  	if sb.strs == nil {
   457  		sb.strs = make(map[string]platformStringInfo)
   458  	}
   459  	si, ok := sb.strs[s]
   460  	if !ok {
   461  		si.set = osSet
   462  		si.osConstraints = make(map[string]bool)
   463  	}
   464  	switch si.set {
   465  	case genericSet:
   466  		return
   467  	case osSet:
   468  		for _, os := range oss {
   469  			si.osConstraints[constraintPrefix+os] = true
   470  		}
   471  	default:
   472  		si.convertToPlatforms(constraintPrefix)
   473  		for _, os := range oss {
   474  			for _, arch := range rule.KnownOSArchs[os] {
   475  				si.platformConstraints[rule.PlatformConstraint{
   476  					Platform:         rule.Platform{OS: os, Arch: arch},
   477  					ConstraintPrefix: constraintPrefix,
   478  				}] = true
   479  			}
   480  		}
   481  	}
   482  	sb.strs[s] = si
   483  }
   484  
   485  func (sb *platformStringsBuilder) addArchString(s string, archs []string, constraintPrefix string) {
   486  	if sb.strs == nil {
   487  		sb.strs = make(map[string]platformStringInfo)
   488  	}
   489  	si, ok := sb.strs[s]
   490  	if !ok {
   491  		si.set = archSet
   492  		si.archConstraints = make(map[string]bool)
   493  	}
   494  	switch si.set {
   495  	case genericSet:
   496  		return
   497  	case archSet:
   498  		for _, arch := range archs {
   499  			si.archConstraints[constraintPrefix+arch] = true
   500  		}
   501  	default:
   502  		si.convertToPlatforms(constraintPrefix)
   503  		for _, arch := range archs {
   504  			for _, os := range rule.KnownArchOSs[arch] {
   505  				si.platformConstraints[rule.PlatformConstraint{
   506  					Platform:         rule.Platform{OS: os, Arch: arch},
   507  					ConstraintPrefix: constraintPrefix,
   508  				}] = true
   509  			}
   510  		}
   511  	}
   512  	sb.strs[s] = si
   513  }
   514  
   515  func (sb *platformStringsBuilder) addPlatformString(s string, platforms []rule.Platform, constraintPrefix string) {
   516  	if sb.strs == nil {
   517  		sb.strs = make(map[string]platformStringInfo)
   518  	}
   519  	si, ok := sb.strs[s]
   520  	if !ok {
   521  		si.set = platformSet
   522  		si.platformConstraints = make(map[rule.PlatformConstraint]bool)
   523  	}
   524  	switch si.set {
   525  	case genericSet:
   526  		return
   527  	default:
   528  		si.convertToPlatforms(constraintPrefix)
   529  		for _, p := range platforms {
   530  			pConstraint := rule.PlatformConstraint{Platform: rule.Platform{OS: p.OS, Arch: p.Arch}, ConstraintPrefix: constraintPrefix}
   531  			si.platformConstraints[pConstraint] = true
   532  		}
   533  	}
   534  	sb.strs[s] = si
   535  }
   536  
   537  func (sb *platformStringsBuilder) build() rule.PlatformStrings {
   538  	var ps rule.PlatformStrings
   539  	for s, si := range sb.strs {
   540  		switch si.set {
   541  		case genericSet:
   542  			ps.Generic = append(ps.Generic, s)
   543  		case osSet:
   544  			if ps.OS == nil {
   545  				ps.OS = make(map[string][]string)
   546  			}
   547  			for os := range si.osConstraints {
   548  				ps.OS[os] = append(ps.OS[os], s)
   549  			}
   550  		case archSet:
   551  			if ps.Arch == nil {
   552  				ps.Arch = make(map[string][]string)
   553  			}
   554  			for arch := range si.archConstraints {
   555  				ps.Arch[arch] = append(ps.Arch[arch], s)
   556  			}
   557  		case platformSet:
   558  			if ps.Platform == nil {
   559  				ps.Platform = make(map[rule.PlatformConstraint][]string)
   560  			}
   561  			for p := range si.platformConstraints {
   562  				ps.Platform[p] = append(ps.Platform[p], s)
   563  			}
   564  		}
   565  	}
   566  	sort.Strings(ps.Generic)
   567  	if ps.OS != nil {
   568  		for _, ss := range ps.OS {
   569  			sort.Strings(ss)
   570  		}
   571  	}
   572  	if ps.Arch != nil {
   573  		for _, ss := range ps.Arch {
   574  			sort.Strings(ss)
   575  		}
   576  	}
   577  	if ps.Platform != nil {
   578  		for _, ss := range ps.Platform {
   579  			sort.Strings(ss)
   580  		}
   581  	}
   582  	return ps
   583  }
   584  
   585  func (sb *platformStringsBuilder) buildFlat() []string {
   586  	strs := make([]string, 0, len(sb.strs))
   587  	for s := range sb.strs {
   588  		strs = append(strs, s)
   589  	}
   590  	sort.Strings(strs)
   591  	return strs
   592  }
   593  
   594  func (si *platformStringInfo) convertToPlatforms(constraintPrefix string) {
   595  	switch si.set {
   596  	case genericSet:
   597  		log.Panic("cannot convert generic string to platformConstraints")
   598  	case platformSet:
   599  		return
   600  	case osSet:
   601  		si.set = platformSet
   602  		si.platformConstraints = make(map[rule.PlatformConstraint]bool)
   603  		for osConstraint := range si.osConstraints {
   604  			os := strings.TrimPrefix(osConstraint, constraintPrefix)
   605  			for _, arch := range rule.KnownOSArchs[os] {
   606  				si.platformConstraints[rule.PlatformConstraint{
   607  					Platform:         rule.Platform{OS: os, Arch: arch},
   608  					ConstraintPrefix: constraintPrefix,
   609  				}] = true
   610  			}
   611  		}
   612  		si.osConstraints = nil
   613  	case archSet:
   614  		si.set = platformSet
   615  		si.platformConstraints = make(map[rule.PlatformConstraint]bool)
   616  		for archConstraint := range si.archConstraints {
   617  			arch := strings.TrimPrefix(archConstraint, constraintPrefix)
   618  			for _, os := range rule.KnownArchOSs[arch] {
   619  				si.platformConstraints[rule.PlatformConstraint{
   620  					Platform:         rule.Platform{OS: os, Arch: arch},
   621  					ConstraintPrefix: constraintPrefix,
   622  				}] = true
   623  			}
   624  		}
   625  		si.archConstraints = nil
   626  	}
   627  }
   628  
   629  var semverRex = regexp.MustCompile(`^.*?(/v\d+)(?:/.*)?$`)
   630  
   631  // pathWithoutSemver removes a semantic version suffix from path.
   632  // For example, if path is "example.com/foo/v2/bar", pathWithoutSemver
   633  // will return "example.com/foo/bar". If there is no semantic version suffix,
   634  // "" will be returned.
   635  func pathWithoutSemver(path string) string {
   636  	m := semverRex.FindStringSubmatchIndex(path)
   637  	if m == nil {
   638  		return ""
   639  	}
   640  	v := path[m[2]+2 : m[3]]
   641  	if v[0] == '0' || v == "1" {
   642  		return ""
   643  	}
   644  	return path[:m[2]] + path[m[3]:]
   645  }