github.com/afking/bazel-gazelle@v0.0.0-20180301150245-c02bc0f529e8/internal/packages/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 packages
    17  
    18  import (
    19  	"fmt"
    20  	"log"
    21  	"path"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/bazelbuild/bazel-gazelle/internal/config"
    26  	"github.com/bazelbuild/bazel-gazelle/internal/pathtools"
    27  )
    28  
    29  // Package contains metadata about a Go package extracted from a directory.
    30  // It fills a similar role to go/build.Package, but it separates files by
    31  // target instead of by type, and it supports multiple platforms.
    32  type Package struct {
    33  	// Name is the symbol found in package declarations of the .go files in
    34  	// the package. It does not include the "_test" suffix from external tests.
    35  	Name string
    36  
    37  	// Dir is an absolute path to the directory that contains the package.
    38  	Dir string
    39  
    40  	// Rel is the relative path to the package directory from the repository
    41  	// root. If the directory is the repository root itself, Rel is empty.
    42  	// Components in Rel are separated with slashes.
    43  	Rel string
    44  
    45  	// ImportPath is the string used to import this package in Go.
    46  	ImportPath string
    47  
    48  	Library, Binary, Test, XTest GoTarget
    49  	Proto                        ProtoTarget
    50  
    51  	HasTestdata bool
    52  }
    53  
    54  // GoTarget contains metadata about a buildable Go target in a package.
    55  type GoTarget struct {
    56  	Sources, Imports PlatformStrings
    57  	COpts, CLinkOpts PlatformStrings
    58  	Cgo              bool
    59  }
    60  
    61  // ProtoTarget contains metadata about proto files in a package.
    62  type ProtoTarget struct {
    63  	Sources, Imports PlatformStrings
    64  	HasServices      bool
    65  
    66  	// HasPbGo indicates whether unexcluded .pb.go files are present in the
    67  	// same package. They will not be in this target's sources.
    68  	HasPbGo bool
    69  }
    70  
    71  // PlatformStrings contains a set of strings associated with a buildable
    72  // Go target in a package. This is used to store source file names,
    73  // import paths, and flags.
    74  //
    75  // Strings are stored in four sets: generic strings, OS-specific strings,
    76  // arch-specific strings, and OS-and-arch-specific strings. A string may not
    77  // be duplicated within a list or across sets; however, a string may appear
    78  // in more than one list within a set (e.g., in "linux" and "windows" within
    79  // the OS set). Strings within each list should be sorted, though this may
    80  // not be relied upon.
    81  type PlatformStrings struct {
    82  	// Generic is a list of strings not specific to any platform.
    83  	Generic []string
    84  
    85  	// OS is a map from OS name (anything in config.KnownOSs) to
    86  	// OS-specific strings.
    87  	OS map[string][]string
    88  
    89  	// Arch is a map from architecture name (anything in config.KnownArchs) to
    90  	// architecture-specific strings.
    91  	Arch map[string][]string
    92  
    93  	// Platform is a map from platforms to OS and architecture-specific strings.
    94  	Platform map[config.Platform][]string
    95  }
    96  
    97  // IsCommand returns true if the package name is "main".
    98  func (p *Package) IsCommand() bool {
    99  	return p.Name == "main"
   100  }
   101  
   102  // EmptyPackage returns an empty package. The package name and import path
   103  // are inferred from the directory name and configuration. This is useful
   104  // for deleting rules in directories which no longer have source files.
   105  func EmptyPackage(c *config.Config, dir, rel string) *Package {
   106  	packageName := pathtools.RelBaseName(rel, c.GoPrefix, c.RepoRoot)
   107  	pb := packageBuilder{
   108  		name: packageName,
   109  		dir:  dir,
   110  		rel:  rel,
   111  	}
   112  	pb.inferImportPath(c)
   113  	return pb.build()
   114  }
   115  
   116  func (t *GoTarget) HasGo() bool {
   117  	return t.Sources.HasGo()
   118  }
   119  
   120  func (t *ProtoTarget) HasProto() bool {
   121  	return !t.Sources.IsEmpty()
   122  }
   123  
   124  func (ps *PlatformStrings) HasGo() bool {
   125  	return ps.firstGoFile() != ""
   126  }
   127  
   128  func (ps *PlatformStrings) IsEmpty() bool {
   129  	return len(ps.Generic) == 0 && len(ps.OS) == 0 && len(ps.Arch) == 0 && len(ps.Platform) == 0
   130  }
   131  
   132  func (ps *PlatformStrings) firstGoFile() string {
   133  	for _, f := range ps.Generic {
   134  		if strings.HasSuffix(f, ".go") {
   135  			return f
   136  		}
   137  	}
   138  	for _, fs := range ps.OS {
   139  		for _, f := range fs {
   140  			if strings.HasSuffix(f, ".go") {
   141  				return f
   142  			}
   143  		}
   144  	}
   145  	for _, fs := range ps.Arch {
   146  		for _, f := range fs {
   147  			if strings.HasSuffix(f, ".go") {
   148  				return f
   149  			}
   150  		}
   151  	}
   152  	for _, fs := range ps.Platform {
   153  		for _, f := range fs {
   154  			if strings.HasSuffix(f, ".go") {
   155  				return f
   156  			}
   157  		}
   158  	}
   159  	return ""
   160  }
   161  
   162  type packageBuilder struct {
   163  	name, dir, rel               string
   164  	library, binary, test, xtest goTargetBuilder
   165  	proto                        protoTargetBuilder
   166  	hasTestdata                  bool
   167  	importPath, importPathFile   string
   168  }
   169  
   170  type goTargetBuilder struct {
   171  	sources, imports, copts, clinkopts platformStringsBuilder
   172  	cgo                                bool
   173  }
   174  
   175  type protoTargetBuilder struct {
   176  	sources, imports     platformStringsBuilder
   177  	hasServices, hasPbGo bool
   178  }
   179  
   180  type platformStringsBuilder struct {
   181  	strs map[string]platformStringInfo
   182  }
   183  
   184  type platformStringInfo struct {
   185  	set       platformStringSet
   186  	oss       map[string]bool
   187  	archs     map[string]bool
   188  	platforms map[config.Platform]bool
   189  }
   190  
   191  type platformStringSet int
   192  
   193  const (
   194  	genericSet platformStringSet = iota
   195  	osSet
   196  	archSet
   197  	platformSet
   198  )
   199  
   200  // addFile adds the file described by "info" to a target in the package "p" if
   201  // the file is buildable.
   202  //
   203  // "cgo" tells whether any ".go" file in the package contains cgo code. This
   204  // affects whether C files are added to targets.
   205  //
   206  // An error is returned if a file is buildable but invalid (for example, a
   207  // test .go file containing cgo code). Files that are not buildable will not
   208  // be added to any target (for example, .txt files).
   209  func (pb *packageBuilder) addFile(c *config.Config, info fileInfo, cgo bool) error {
   210  	switch {
   211  	case info.category == ignoredExt || info.category == unsupportedExt ||
   212  		!cgo && (info.category == cExt || info.category == csExt) ||
   213  		c.ProtoMode == config.DisableProtoMode && info.category == protoExt:
   214  		return nil
   215  	case info.isXTest:
   216  		if info.isCgo {
   217  			return fmt.Errorf("%s: use of cgo in test not supported", info.path)
   218  		}
   219  		pb.xtest.addFile(c, info)
   220  	case info.isTest:
   221  		if info.isCgo {
   222  			return fmt.Errorf("%s: use of cgo in test not supported", info.path)
   223  		}
   224  		pb.test.addFile(c, info)
   225  	case info.category == protoExt:
   226  		pb.proto.addFile(c, info)
   227  	default:
   228  		pb.library.addFile(c, info)
   229  	}
   230  	if strings.HasSuffix(info.name, ".pb.go") {
   231  		pb.proto.hasPbGo = true
   232  	}
   233  
   234  	if info.importPath != "" {
   235  		if pb.importPath == "" {
   236  			pb.importPath = info.importPath
   237  			pb.importPathFile = info.path
   238  		} else if pb.importPath != info.importPath {
   239  			return fmt.Errorf("found import comments %q (%s) and %q (%s)", pb.importPath, pb.importPathFile, info.importPath, info.path)
   240  		}
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  // isBuildable returns true if anything in the package is buildable.
   247  // This is true if the package has Go code that satisfies build constraints
   248  // on any platform or has proto files not in legacy mode.
   249  func (pb *packageBuilder) isBuildable(c *config.Config) bool {
   250  	return pb.firstGoFile() != "" ||
   251  		len(pb.proto.sources.strs) > 0 && c.ProtoMode == config.DefaultProtoMode
   252  }
   253  
   254  // firstGoFile returns the name of a .go file if the package contains at least
   255  // one .go file, or "" otherwise.
   256  func (pb *packageBuilder) firstGoFile() string {
   257  	goSrcs := []platformStringsBuilder{
   258  		pb.library.sources,
   259  		pb.binary.sources,
   260  		pb.test.sources,
   261  		pb.xtest.sources,
   262  	}
   263  	for _, sb := range goSrcs {
   264  		if sb.strs != nil {
   265  			for s, _ := range sb.strs {
   266  				if strings.HasSuffix(s, ".go") {
   267  					return s
   268  				}
   269  			}
   270  		}
   271  	}
   272  	return ""
   273  }
   274  
   275  func (pb *packageBuilder) inferImportPath(c *config.Config) error {
   276  	if pb.importPath != "" {
   277  		log.Panic("importPath already set")
   278  	}
   279  	if pb.rel == c.GoPrefixRel {
   280  		if c.GoPrefix == "" {
   281  			return fmt.Errorf("in directory %q, prefix is empty, so importpath would be empty for rules. Set a prefix with a '# gazelle:prefix' comment or with -go_prefix on the command line.", pb.dir)
   282  		}
   283  		pb.importPath = c.GoPrefix
   284  	} else {
   285  		fromPrefixRel := strings.TrimPrefix(pb.rel, c.GoPrefixRel+"/")
   286  		pb.importPath = path.Join(c.GoPrefix, fromPrefixRel)
   287  	}
   288  	return nil
   289  }
   290  
   291  func (pb *packageBuilder) build() *Package {
   292  	return &Package{
   293  		Name:        pb.name,
   294  		Dir:         pb.dir,
   295  		Rel:         pb.rel,
   296  		ImportPath:  pb.importPath,
   297  		Library:     pb.library.build(),
   298  		Binary:      pb.binary.build(),
   299  		Test:        pb.test.build(),
   300  		XTest:       pb.xtest.build(),
   301  		Proto:       pb.proto.build(),
   302  		HasTestdata: pb.hasTestdata,
   303  	}
   304  }
   305  
   306  func (tb *goTargetBuilder) addFile(c *config.Config, info fileInfo) {
   307  	tb.cgo = tb.cgo || info.isCgo
   308  	add := getPlatformStringsAddFunction(c, info, nil)
   309  	add(&tb.sources, info.name)
   310  	add(&tb.imports, info.imports...)
   311  	for _, copts := range info.copts {
   312  		optAdd := add
   313  		if len(copts.tags) > 0 {
   314  			optAdd = getPlatformStringsAddFunction(c, info, copts.tags)
   315  		}
   316  		optAdd(&tb.copts, copts.opts)
   317  	}
   318  	for _, clinkopts := range info.clinkopts {
   319  		optAdd := add
   320  		if len(clinkopts.tags) > 0 {
   321  			optAdd = getPlatformStringsAddFunction(c, info, clinkopts.tags)
   322  		}
   323  		optAdd(&tb.clinkopts, clinkopts.opts)
   324  	}
   325  }
   326  
   327  func (tb *goTargetBuilder) build() GoTarget {
   328  	return GoTarget{
   329  		Sources:   tb.sources.build(),
   330  		Imports:   tb.imports.build(),
   331  		COpts:     tb.copts.build(),
   332  		CLinkOpts: tb.clinkopts.build(),
   333  		Cgo:       tb.cgo,
   334  	}
   335  }
   336  
   337  func (tb *protoTargetBuilder) addFile(c *config.Config, info fileInfo) {
   338  	add := getPlatformStringsAddFunction(c, info, nil)
   339  	add(&tb.sources, info.name)
   340  	add(&tb.imports, info.imports...)
   341  	tb.hasServices = tb.hasServices || info.hasServices
   342  }
   343  
   344  func (tb *protoTargetBuilder) build() ProtoTarget {
   345  	return ProtoTarget{
   346  		Sources:     tb.sources.build(),
   347  		Imports:     tb.imports.build(),
   348  		HasServices: tb.hasServices,
   349  		HasPbGo:     tb.hasPbGo,
   350  	}
   351  }
   352  
   353  // getPlatformStringsAddFunction returns a function used to add strings to
   354  // a *platformStringsBuilder under the same set of constraints. This is a
   355  // performance optimization to avoid evaluating constraints repeatedly.
   356  func getPlatformStringsAddFunction(c *config.Config, info fileInfo, cgoTags tagLine) func(sb *platformStringsBuilder, ss ...string) {
   357  	isOSSpecific, isArchSpecific := isOSArchSpecific(info, cgoTags)
   358  
   359  	switch {
   360  	case !isOSSpecific && !isArchSpecific:
   361  		if checkConstraints(c, "", "", info.goos, info.goarch, info.tags, cgoTags) {
   362  			return func(sb *platformStringsBuilder, ss ...string) {
   363  				for _, s := range ss {
   364  					sb.addGenericString(s)
   365  				}
   366  			}
   367  		}
   368  
   369  	case isOSSpecific && !isArchSpecific:
   370  		var osMatch []string
   371  		for _, os := range config.KnownOSs {
   372  			if checkConstraints(c, os, "", info.goos, info.goarch, info.tags, cgoTags) {
   373  				osMatch = append(osMatch, os)
   374  			}
   375  		}
   376  		if len(osMatch) > 0 {
   377  			return func(sb *platformStringsBuilder, ss ...string) {
   378  				for _, s := range ss {
   379  					sb.addOSString(s, osMatch)
   380  				}
   381  			}
   382  		}
   383  
   384  	case !isOSSpecific && isArchSpecific:
   385  		var archMatch []string
   386  		for _, arch := range config.KnownArchs {
   387  			if checkConstraints(c, "", arch, info.goos, info.goarch, info.tags, cgoTags) {
   388  				archMatch = append(archMatch, arch)
   389  			}
   390  		}
   391  		if len(archMatch) > 0 {
   392  			return func(sb *platformStringsBuilder, ss ...string) {
   393  				for _, s := range ss {
   394  					sb.addArchString(s, archMatch)
   395  				}
   396  			}
   397  		}
   398  
   399  	default:
   400  		var platformMatch []config.Platform
   401  		for _, platform := range config.KnownPlatforms {
   402  			if checkConstraints(c, platform.OS, platform.Arch, info.goos, info.goarch, info.tags, cgoTags) {
   403  				platformMatch = append(platformMatch, platform)
   404  			}
   405  		}
   406  		if len(platformMatch) > 0 {
   407  			return func(sb *platformStringsBuilder, ss ...string) {
   408  				for _, s := range ss {
   409  					sb.addPlatformString(s, platformMatch)
   410  				}
   411  			}
   412  		}
   413  	}
   414  
   415  	return func(_ *platformStringsBuilder, _ ...string) {}
   416  }
   417  
   418  func (sb *platformStringsBuilder) addGenericString(s string) {
   419  	if sb.strs == nil {
   420  		sb.strs = make(map[string]platformStringInfo)
   421  	}
   422  	sb.strs[s] = platformStringInfo{set: genericSet}
   423  }
   424  
   425  func (sb *platformStringsBuilder) addOSString(s string, oss []string) {
   426  	if sb.strs == nil {
   427  		sb.strs = make(map[string]platformStringInfo)
   428  	}
   429  	si, ok := sb.strs[s]
   430  	if !ok {
   431  		si.set = osSet
   432  		si.oss = make(map[string]bool)
   433  	}
   434  	switch si.set {
   435  	case genericSet:
   436  		return
   437  	case osSet:
   438  		for _, os := range oss {
   439  			si.oss[os] = true
   440  		}
   441  	default:
   442  		si.convertToPlatforms()
   443  		for _, os := range oss {
   444  			for _, arch := range config.KnownOSArchs[os] {
   445  				si.platforms[config.Platform{OS: os, Arch: arch}] = true
   446  			}
   447  		}
   448  	}
   449  	sb.strs[s] = si
   450  }
   451  
   452  func (sb *platformStringsBuilder) addArchString(s string, archs []string) {
   453  	if sb.strs == nil {
   454  		sb.strs = make(map[string]platformStringInfo)
   455  	}
   456  	si, ok := sb.strs[s]
   457  	if !ok {
   458  		si.set = archSet
   459  		si.archs = make(map[string]bool)
   460  	}
   461  	switch si.set {
   462  	case genericSet:
   463  		return
   464  	case archSet:
   465  		for _, arch := range archs {
   466  			si.archs[arch] = true
   467  		}
   468  	default:
   469  		si.convertToPlatforms()
   470  		for _, arch := range archs {
   471  			for _, os := range config.KnownArchOSs[arch] {
   472  				si.platforms[config.Platform{OS: os, Arch: arch}] = true
   473  			}
   474  		}
   475  	}
   476  	sb.strs[s] = si
   477  }
   478  
   479  func (sb *platformStringsBuilder) addPlatformString(s string, platforms []config.Platform) {
   480  	if sb.strs == nil {
   481  		sb.strs = make(map[string]platformStringInfo)
   482  	}
   483  	si, ok := sb.strs[s]
   484  	if !ok {
   485  		si.set = platformSet
   486  		si.platforms = make(map[config.Platform]bool)
   487  	}
   488  	switch si.set {
   489  	case genericSet:
   490  		return
   491  	default:
   492  		si.convertToPlatforms()
   493  		for _, p := range platforms {
   494  			si.platforms[p] = true
   495  		}
   496  	}
   497  	sb.strs[s] = si
   498  }
   499  
   500  func (sb *platformStringsBuilder) build() PlatformStrings {
   501  	var ps PlatformStrings
   502  	for s, si := range sb.strs {
   503  		switch si.set {
   504  		case genericSet:
   505  			ps.Generic = append(ps.Generic, s)
   506  		case osSet:
   507  			if ps.OS == nil {
   508  				ps.OS = make(map[string][]string)
   509  			}
   510  			for os, _ := range si.oss {
   511  				ps.OS[os] = append(ps.OS[os], s)
   512  			}
   513  		case archSet:
   514  			if ps.Arch == nil {
   515  				ps.Arch = make(map[string][]string)
   516  			}
   517  			for arch, _ := range si.archs {
   518  				ps.Arch[arch] = append(ps.Arch[arch], s)
   519  			}
   520  		case platformSet:
   521  			if ps.Platform == nil {
   522  				ps.Platform = make(map[config.Platform][]string)
   523  			}
   524  			for p, _ := range si.platforms {
   525  				ps.Platform[p] = append(ps.Platform[p], s)
   526  			}
   527  		}
   528  	}
   529  	sort.Strings(ps.Generic)
   530  	if ps.OS != nil {
   531  		for _, ss := range ps.OS {
   532  			sort.Strings(ss)
   533  		}
   534  	}
   535  	if ps.Arch != nil {
   536  		for _, ss := range ps.Arch {
   537  			sort.Strings(ss)
   538  		}
   539  	}
   540  	if ps.Platform != nil {
   541  		for _, ss := range ps.Platform {
   542  			sort.Strings(ss)
   543  		}
   544  	}
   545  	return ps
   546  }
   547  
   548  func (si *platformStringInfo) convertToPlatforms() {
   549  	switch si.set {
   550  	case genericSet:
   551  		log.Panic("cannot convert generic string to platforms")
   552  	case platformSet:
   553  		return
   554  	case osSet:
   555  		si.set = platformSet
   556  		si.platforms = make(map[config.Platform]bool)
   557  		for os, _ := range si.oss {
   558  			for _, arch := range config.KnownOSArchs[os] {
   559  				si.platforms[config.Platform{OS: os, Arch: arch}] = true
   560  			}
   561  		}
   562  		si.oss = nil
   563  	case archSet:
   564  		si.set = platformSet
   565  		si.platforms = make(map[config.Platform]bool)
   566  		for arch, _ := range si.archs {
   567  			for _, os := range config.KnownArchOSs[arch] {
   568  				si.platforms[config.Platform{OS: os, Arch: arch}] = true
   569  			}
   570  		}
   571  		si.archs = nil
   572  	}
   573  }
   574  
   575  // MapSlice applies a function that processes slices of strings to the strings
   576  // in "ps" and returns a new PlatformStrings with the results.
   577  func (ps *PlatformStrings) MapSlice(f func([]string) ([]string, error)) (PlatformStrings, []error) {
   578  	var errors []error
   579  
   580  	mapSlice := func(ss []string) []string {
   581  		rs, err := f(ss)
   582  		if err != nil {
   583  			errors = append(errors, err)
   584  			return nil
   585  		}
   586  		return rs
   587  	}
   588  
   589  	mapStringMap := func(m map[string][]string) map[string][]string {
   590  		if m == nil {
   591  			return nil
   592  		}
   593  		rm := make(map[string][]string)
   594  		for k, ss := range m {
   595  			ss = mapSlice(ss)
   596  			if len(ss) > 0 {
   597  				rm[k] = ss
   598  			}
   599  		}
   600  		if len(rm) == 0 {
   601  			return nil
   602  		}
   603  		return rm
   604  	}
   605  
   606  	mapPlatformMap := func(m map[config.Platform][]string) map[config.Platform][]string {
   607  		if m == nil {
   608  			return nil
   609  		}
   610  		rm := make(map[config.Platform][]string)
   611  		for k, ss := range m {
   612  			ss = mapSlice(ss)
   613  			if len(ss) > 0 {
   614  				rm[k] = ss
   615  			}
   616  		}
   617  		if len(rm) == 0 {
   618  			return nil
   619  		}
   620  		return rm
   621  	}
   622  
   623  	result := PlatformStrings{
   624  		Generic:  mapSlice(ps.Generic),
   625  		OS:       mapStringMap(ps.OS),
   626  		Arch:     mapStringMap(ps.Arch),
   627  		Platform: mapPlatformMap(ps.Platform),
   628  	}
   629  	return result, errors
   630  }