| /* Copyright 2017 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 packages |
| |
| import ( |
| "fmt" |
| "log" |
| "path" |
| "sort" |
| "strings" |
| |
| "github.com/bazelbuild/bazel-gazelle/internal/config" |
| "github.com/bazelbuild/bazel-gazelle/internal/pathtools" |
| ) |
| |
| // Package contains metadata about a Go package extracted from a directory. |
| // It fills a similar role to go/build.Package, but it separates files by |
| // target instead of by type, and it supports multiple platforms. |
| type Package struct { |
| // Name is the symbol found in package declarations of the .go files in |
| // the package. It does not include the "_test" suffix from external tests. |
| Name string |
| |
| // Dir is an absolute path to the directory that contains the package. |
| Dir string |
| |
| // Rel is the relative path to the package directory from the repository |
| // root. If the directory is the repository root itself, Rel is empty. |
| // Components in Rel are separated with slashes. |
| Rel string |
| |
| // ImportPath is the string used to import this package in Go. |
| ImportPath string |
| |
| Library, Binary, Test, XTest GoTarget |
| Proto ProtoTarget |
| |
| HasTestdata bool |
| } |
| |
| // GoTarget contains metadata about a buildable Go target in a package. |
| type GoTarget struct { |
| Sources, Imports PlatformStrings |
| COpts, CLinkOpts PlatformStrings |
| Cgo bool |
| } |
| |
| // ProtoTarget contains metadata about proto files in a package. |
| type ProtoTarget struct { |
| Sources, Imports PlatformStrings |
| HasServices bool |
| |
| // HasPbGo indicates whether unexcluded .pb.go files are present in the |
| // same package. They will not be in this target's sources. |
| HasPbGo bool |
| } |
| |
| // PlatformStrings contains a set of strings associated with a buildable |
| // Go target in a package. This is used to store source file names, |
| // import paths, and flags. |
| // |
| // Strings are stored in four sets: generic strings, OS-specific strings, |
| // arch-specific strings, and OS-and-arch-specific strings. A string may not |
| // be duplicated within a list or across sets; however, a string may appear |
| // in more than one list within a set (e.g., in "linux" and "windows" within |
| // the OS set). Strings within each list should be sorted, though this may |
| // not be relied upon. |
| type PlatformStrings struct { |
| // Generic is a list of strings not specific to any platform. |
| Generic []string |
| |
| // OS is a map from OS name (anything in config.KnownOSs) to |
| // OS-specific strings. |
| OS map[string][]string |
| |
| // Arch is a map from architecture name (anything in config.KnownArchs) to |
| // architecture-specific strings. |
| Arch map[string][]string |
| |
| // Platform is a map from platforms to OS and architecture-specific strings. |
| Platform map[config.Platform][]string |
| } |
| |
| // IsCommand returns true if the package name is "main". |
| func (p *Package) IsCommand() bool { |
| return p.Name == "main" |
| } |
| |
| // EmptyPackage returns an empty package. The package name and import path |
| // are inferred from the directory name and configuration. This is useful |
| // for deleting rules in directories which no longer have source files. |
| func EmptyPackage(c *config.Config, dir, rel string) *Package { |
| packageName := pathtools.RelBaseName(rel, c.GoPrefix, c.RepoRoot) |
| pb := packageBuilder{ |
| name: packageName, |
| dir: dir, |
| rel: rel, |
| } |
| pb.inferImportPath(c) |
| return pb.build() |
| } |
| |
| func (t *GoTarget) HasGo() bool { |
| return t.Sources.HasGo() |
| } |
| |
| func (t *ProtoTarget) HasProto() bool { |
| return !t.Sources.IsEmpty() |
| } |
| |
| func (ps *PlatformStrings) HasGo() bool { |
| return ps.firstGoFile() != "" |
| } |
| |
| func (ps *PlatformStrings) IsEmpty() bool { |
| return len(ps.Generic) == 0 && len(ps.OS) == 0 && len(ps.Arch) == 0 && len(ps.Platform) == 0 |
| } |
| |
| func (ps *PlatformStrings) firstGoFile() string { |
| for _, f := range ps.Generic { |
| if strings.HasSuffix(f, ".go") { |
| return f |
| } |
| } |
| for _, fs := range ps.OS { |
| for _, f := range fs { |
| if strings.HasSuffix(f, ".go") { |
| return f |
| } |
| } |
| } |
| for _, fs := range ps.Arch { |
| for _, f := range fs { |
| if strings.HasSuffix(f, ".go") { |
| return f |
| } |
| } |
| } |
| for _, fs := range ps.Platform { |
| for _, f := range fs { |
| if strings.HasSuffix(f, ".go") { |
| return f |
| } |
| } |
| } |
| return "" |
| } |
| |
| type packageBuilder struct { |
| name, dir, rel string |
| library, binary, test, xtest goTargetBuilder |
| proto protoTargetBuilder |
| hasTestdata bool |
| importPath, importPathFile string |
| } |
| |
| type goTargetBuilder struct { |
| sources, imports, copts, clinkopts platformStringsBuilder |
| cgo bool |
| } |
| |
| type protoTargetBuilder struct { |
| sources, imports platformStringsBuilder |
| hasServices, hasPbGo bool |
| } |
| |
| type platformStringsBuilder struct { |
| strs map[string]platformStringInfo |
| } |
| |
| type platformStringInfo struct { |
| set platformStringSet |
| oss map[string]bool |
| archs map[string]bool |
| platforms map[config.Platform]bool |
| } |
| |
| type platformStringSet int |
| |
| const ( |
| genericSet platformStringSet = iota |
| osSet |
| archSet |
| platformSet |
| ) |
| |
| // addFile adds the file described by "info" to a target in the package "p" if |
| // the file is buildable. |
| // |
| // "cgo" tells whether any ".go" file in the package contains cgo code. This |
| // affects whether C files are added to targets. |
| // |
| // An error is returned if a file is buildable but invalid (for example, a |
| // test .go file containing cgo code). Files that are not buildable will not |
| // be added to any target (for example, .txt files). |
| func (pb *packageBuilder) addFile(c *config.Config, info fileInfo, cgo bool) error { |
| switch { |
| case info.category == ignoredExt || info.category == unsupportedExt || |
| !cgo && (info.category == cExt || info.category == csExt) || |
| c.ProtoMode == config.DisableProtoMode && info.category == protoExt: |
| return nil |
| case info.isXTest: |
| if info.isCgo { |
| return fmt.Errorf("%s: use of cgo in test not supported", info.path) |
| } |
| pb.xtest.addFile(c, info) |
| case info.isTest: |
| if info.isCgo { |
| return fmt.Errorf("%s: use of cgo in test not supported", info.path) |
| } |
| pb.test.addFile(c, info) |
| case info.category == protoExt: |
| pb.proto.addFile(c, info) |
| default: |
| pb.library.addFile(c, info) |
| } |
| if strings.HasSuffix(info.name, ".pb.go") { |
| pb.proto.hasPbGo = true |
| } |
| |
| if info.importPath != "" { |
| if pb.importPath == "" { |
| pb.importPath = info.importPath |
| pb.importPathFile = info.path |
| } else if pb.importPath != info.importPath { |
| return fmt.Errorf("found import comments %q (%s) and %q (%s)", pb.importPath, pb.importPathFile, info.importPath, info.path) |
| } |
| } |
| |
| return nil |
| } |
| |
| // isBuildable returns true if anything in the package is buildable. |
| // This is true if the package has Go code that satisfies build constraints |
| // on any platform or has proto files not in legacy mode. |
| func (pb *packageBuilder) isBuildable(c *config.Config) bool { |
| return pb.firstGoFile() != "" || |
| len(pb.proto.sources.strs) > 0 && c.ProtoMode == config.DefaultProtoMode |
| } |
| |
| // firstGoFile returns the name of a .go file if the package contains at least |
| // one .go file, or "" otherwise. |
| func (pb *packageBuilder) firstGoFile() string { |
| goSrcs := []platformStringsBuilder{ |
| pb.library.sources, |
| pb.binary.sources, |
| pb.test.sources, |
| pb.xtest.sources, |
| } |
| for _, sb := range goSrcs { |
| if sb.strs != nil { |
| for s, _ := range sb.strs { |
| if strings.HasSuffix(s, ".go") { |
| return s |
| } |
| } |
| } |
| } |
| return "" |
| } |
| |
| func (pb *packageBuilder) inferImportPath(c *config.Config) error { |
| if pb.importPath != "" { |
| log.Panic("importPath already set") |
| } |
| if pb.rel == c.GoPrefixRel { |
| if c.GoPrefix == "" { |
| 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) |
| } |
| pb.importPath = c.GoPrefix |
| } else { |
| fromPrefixRel := strings.TrimPrefix(pb.rel, c.GoPrefixRel+"/") |
| pb.importPath = path.Join(c.GoPrefix, fromPrefixRel) |
| } |
| return nil |
| } |
| |
| func (pb *packageBuilder) build() *Package { |
| return &Package{ |
| Name: pb.name, |
| Dir: pb.dir, |
| Rel: pb.rel, |
| ImportPath: pb.importPath, |
| Library: pb.library.build(), |
| Binary: pb.binary.build(), |
| Test: pb.test.build(), |
| XTest: pb.xtest.build(), |
| Proto: pb.proto.build(), |
| HasTestdata: pb.hasTestdata, |
| } |
| } |
| |
| func (tb *goTargetBuilder) addFile(c *config.Config, info fileInfo) { |
| tb.cgo = tb.cgo || info.isCgo |
| add := getPlatformStringsAddFunction(c, info, nil) |
| add(&tb.sources, info.name) |
| add(&tb.imports, info.imports...) |
| for _, copts := range info.copts { |
| optAdd := add |
| if len(copts.tags) > 0 { |
| optAdd = getPlatformStringsAddFunction(c, info, copts.tags) |
| } |
| optAdd(&tb.copts, copts.opts) |
| } |
| for _, clinkopts := range info.clinkopts { |
| optAdd := add |
| if len(clinkopts.tags) > 0 { |
| optAdd = getPlatformStringsAddFunction(c, info, clinkopts.tags) |
| } |
| optAdd(&tb.clinkopts, clinkopts.opts) |
| } |
| } |
| |
| func (tb *goTargetBuilder) build() GoTarget { |
| return GoTarget{ |
| Sources: tb.sources.build(), |
| Imports: tb.imports.build(), |
| COpts: tb.copts.build(), |
| CLinkOpts: tb.clinkopts.build(), |
| Cgo: tb.cgo, |
| } |
| } |
| |
| func (tb *protoTargetBuilder) addFile(c *config.Config, info fileInfo) { |
| add := getPlatformStringsAddFunction(c, info, nil) |
| add(&tb.sources, info.name) |
| add(&tb.imports, info.imports...) |
| tb.hasServices = tb.hasServices || info.hasServices |
| } |
| |
| func (tb *protoTargetBuilder) build() ProtoTarget { |
| return ProtoTarget{ |
| Sources: tb.sources.build(), |
| Imports: tb.imports.build(), |
| HasServices: tb.hasServices, |
| HasPbGo: tb.hasPbGo, |
| } |
| } |
| |
| // getPlatformStringsAddFunction returns a function used to add strings to |
| // a *platformStringsBuilder under the same set of constraints. This is a |
| // performance optimization to avoid evaluating constraints repeatedly. |
| func getPlatformStringsAddFunction(c *config.Config, info fileInfo, cgoTags tagLine) func(sb *platformStringsBuilder, ss ...string) { |
| isOSSpecific, isArchSpecific := isOSArchSpecific(info, cgoTags) |
| |
| switch { |
| case !isOSSpecific && !isArchSpecific: |
| if checkConstraints(c, "", "", info.goos, info.goarch, info.tags, cgoTags) { |
| return func(sb *platformStringsBuilder, ss ...string) { |
| for _, s := range ss { |
| sb.addGenericString(s) |
| } |
| } |
| } |
| |
| case isOSSpecific && !isArchSpecific: |
| var osMatch []string |
| for _, os := range config.KnownOSs { |
| if checkConstraints(c, os, "", info.goos, info.goarch, info.tags, cgoTags) { |
| osMatch = append(osMatch, os) |
| } |
| } |
| if len(osMatch) > 0 { |
| return func(sb *platformStringsBuilder, ss ...string) { |
| for _, s := range ss { |
| sb.addOSString(s, osMatch) |
| } |
| } |
| } |
| |
| case !isOSSpecific && isArchSpecific: |
| var archMatch []string |
| for _, arch := range config.KnownArchs { |
| if checkConstraints(c, "", arch, info.goos, info.goarch, info.tags, cgoTags) { |
| archMatch = append(archMatch, arch) |
| } |
| } |
| if len(archMatch) > 0 { |
| return func(sb *platformStringsBuilder, ss ...string) { |
| for _, s := range ss { |
| sb.addArchString(s, archMatch) |
| } |
| } |
| } |
| |
| default: |
| var platformMatch []config.Platform |
| for _, platform := range config.KnownPlatforms { |
| if checkConstraints(c, platform.OS, platform.Arch, info.goos, info.goarch, info.tags, cgoTags) { |
| platformMatch = append(platformMatch, platform) |
| } |
| } |
| if len(platformMatch) > 0 { |
| return func(sb *platformStringsBuilder, ss ...string) { |
| for _, s := range ss { |
| sb.addPlatformString(s, platformMatch) |
| } |
| } |
| } |
| } |
| |
| return func(_ *platformStringsBuilder, _ ...string) {} |
| } |
| |
| func (sb *platformStringsBuilder) addGenericString(s string) { |
| if sb.strs == nil { |
| sb.strs = make(map[string]platformStringInfo) |
| } |
| sb.strs[s] = platformStringInfo{set: genericSet} |
| } |
| |
| func (sb *platformStringsBuilder) addOSString(s string, oss []string) { |
| if sb.strs == nil { |
| sb.strs = make(map[string]platformStringInfo) |
| } |
| si, ok := sb.strs[s] |
| if !ok { |
| si.set = osSet |
| si.oss = make(map[string]bool) |
| } |
| switch si.set { |
| case genericSet: |
| return |
| case osSet: |
| for _, os := range oss { |
| si.oss[os] = true |
| } |
| default: |
| si.convertToPlatforms() |
| for _, os := range oss { |
| for _, arch := range config.KnownOSArchs[os] { |
| si.platforms[config.Platform{OS: os, Arch: arch}] = true |
| } |
| } |
| } |
| sb.strs[s] = si |
| } |
| |
| func (sb *platformStringsBuilder) addArchString(s string, archs []string) { |
| if sb.strs == nil { |
| sb.strs = make(map[string]platformStringInfo) |
| } |
| si, ok := sb.strs[s] |
| if !ok { |
| si.set = archSet |
| si.archs = make(map[string]bool) |
| } |
| switch si.set { |
| case genericSet: |
| return |
| case archSet: |
| for _, arch := range archs { |
| si.archs[arch] = true |
| } |
| default: |
| si.convertToPlatforms() |
| for _, arch := range archs { |
| for _, os := range config.KnownArchOSs[arch] { |
| si.platforms[config.Platform{OS: os, Arch: arch}] = true |
| } |
| } |
| } |
| sb.strs[s] = si |
| } |
| |
| func (sb *platformStringsBuilder) addPlatformString(s string, platforms []config.Platform) { |
| if sb.strs == nil { |
| sb.strs = make(map[string]platformStringInfo) |
| } |
| si, ok := sb.strs[s] |
| if !ok { |
| si.set = platformSet |
| si.platforms = make(map[config.Platform]bool) |
| } |
| switch si.set { |
| case genericSet: |
| return |
| default: |
| si.convertToPlatforms() |
| for _, p := range platforms { |
| si.platforms[p] = true |
| } |
| } |
| sb.strs[s] = si |
| } |
| |
| func (sb *platformStringsBuilder) build() PlatformStrings { |
| var ps PlatformStrings |
| for s, si := range sb.strs { |
| switch si.set { |
| case genericSet: |
| ps.Generic = append(ps.Generic, s) |
| case osSet: |
| if ps.OS == nil { |
| ps.OS = make(map[string][]string) |
| } |
| for os, _ := range si.oss { |
| ps.OS[os] = append(ps.OS[os], s) |
| } |
| case archSet: |
| if ps.Arch == nil { |
| ps.Arch = make(map[string][]string) |
| } |
| for arch, _ := range si.archs { |
| ps.Arch[arch] = append(ps.Arch[arch], s) |
| } |
| case platformSet: |
| if ps.Platform == nil { |
| ps.Platform = make(map[config.Platform][]string) |
| } |
| for p, _ := range si.platforms { |
| ps.Platform[p] = append(ps.Platform[p], s) |
| } |
| } |
| } |
| sort.Strings(ps.Generic) |
| if ps.OS != nil { |
| for _, ss := range ps.OS { |
| sort.Strings(ss) |
| } |
| } |
| if ps.Arch != nil { |
| for _, ss := range ps.Arch { |
| sort.Strings(ss) |
| } |
| } |
| if ps.Platform != nil { |
| for _, ss := range ps.Platform { |
| sort.Strings(ss) |
| } |
| } |
| return ps |
| } |
| |
| func (si *platformStringInfo) convertToPlatforms() { |
| switch si.set { |
| case genericSet: |
| log.Panic("cannot convert generic string to platforms") |
| case platformSet: |
| return |
| case osSet: |
| si.set = platformSet |
| si.platforms = make(map[config.Platform]bool) |
| for os, _ := range si.oss { |
| for _, arch := range config.KnownOSArchs[os] { |
| si.platforms[config.Platform{OS: os, Arch: arch}] = true |
| } |
| } |
| si.oss = nil |
| case archSet: |
| si.set = platformSet |
| si.platforms = make(map[config.Platform]bool) |
| for arch, _ := range si.archs { |
| for _, os := range config.KnownArchOSs[arch] { |
| si.platforms[config.Platform{OS: os, Arch: arch}] = true |
| } |
| } |
| si.archs = nil |
| } |
| } |
| |
| // MapSlice applies a function that processes slices of strings to the strings |
| // in "ps" and returns a new PlatformStrings with the results. |
| func (ps *PlatformStrings) MapSlice(f func([]string) ([]string, error)) (PlatformStrings, []error) { |
| var errors []error |
| |
| mapSlice := func(ss []string) []string { |
| rs, err := f(ss) |
| if err != nil { |
| errors = append(errors, err) |
| return nil |
| } |
| return rs |
| } |
| |
| mapStringMap := func(m map[string][]string) map[string][]string { |
| if m == nil { |
| return nil |
| } |
| rm := make(map[string][]string) |
| for k, ss := range m { |
| ss = mapSlice(ss) |
| if len(ss) > 0 { |
| rm[k] = ss |
| } |
| } |
| if len(rm) == 0 { |
| return nil |
| } |
| return rm |
| } |
| |
| mapPlatformMap := func(m map[config.Platform][]string) map[config.Platform][]string { |
| if m == nil { |
| return nil |
| } |
| rm := make(map[config.Platform][]string) |
| for k, ss := range m { |
| ss = mapSlice(ss) |
| if len(ss) > 0 { |
| rm[k] = ss |
| } |
| } |
| if len(rm) == 0 { |
| return nil |
| } |
| return rm |
| } |
| |
| result := PlatformStrings{ |
| Generic: mapSlice(ps.Generic), |
| OS: mapStringMap(ps.OS), |
| Arch: mapStringMap(ps.Arch), |
| Platform: mapPlatformMap(ps.Platform), |
| } |
| return result, errors |
| } |