blob: dc160671c3868d29985c9d9c0f705f9238ad67be [file] [log] [blame] [edit]
/* 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 golang
import (
"fmt"
"log"
"path"
"regexp"
"sort"
"strings"
"github.com/bazelbuild/bazel-gazelle/config"
"github.com/bazelbuild/bazel-gazelle/language/proto"
"github.com/bazelbuild/bazel-gazelle/pathtools"
"github.com/bazelbuild/bazel-gazelle/rule"
)
// goPackage contains metadata for a set of .go and .proto files that can be
// used to generate Go rules.
type goPackage struct {
name, dir, rel string
library, binary, test goTarget
tests []goTarget
proto protoTarget
hasTestdata bool
hasMainFunction bool
importPath string
}
// goTarget contains information used to generate an individual Go rule
// (library, binary, or test).
type goTarget struct {
sources, embedSrcs, imports, cppopts, copts, cxxopts, clinkopts platformStringsBuilder
cgo, hasInternalTest bool
}
// protoTarget contains information used to generate a go_proto_library rule.
type protoTarget struct {
name string
sources platformStringsBuilder
imports platformStringsBuilder
hasServices bool
}
// platformStringsBuilder is used to construct rule.PlatformStrings. Bazel
// has some requirements for deps list (a dependency cannot appear in more
// than one select expression; dependencies cannot be duplicated), so we need
// to build these carefully.
type platformStringsBuilder struct {
strs map[string]platformStringInfo
}
// platformStringInfo contains information about a single string (source,
// import, or option).
type platformStringInfo struct {
set platformStringSet
osConstraints map[string]bool
archConstraints map[string]bool
platformConstraints map[rule.PlatformConstraint]bool
}
type platformStringSet int
const (
genericSet platformStringSet = iota
osSet
archSet
platformSet
)
// Matches a package version, eg. the end segment of 'example.com/foo/v1'
var pkgVersionRe = regexp.MustCompile("^v[0-9]+$")
// 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 (pkg *goPackage) addFile(c *config.Config, er *embedResolver, info fileInfo, cgo bool) error {
switch {
case info.ext == unknownExt || !cgo && (info.ext == cExt || info.ext == csExt):
return nil
case info.ext == protoExt:
if pcMode := getProtoMode(c); pcMode == proto.LegacyMode {
// Only add files in legacy mode. This is used to generate a filegroup
// that contains all protos. In order modes, we get the .proto files
// from information emitted by the proto language extension.
pkg.proto.addFile(info)
}
case info.isTest:
if getGoConfig(c).testMode == fileTestMode || len(pkg.tests) == 0 {
pkg.tests = append(pkg.tests, goTarget{})
}
// Add the the file to the most recently added test target (in fileTestMode)
// or the only test target (in defaultMode).
// In both cases, this will be the last element in the slice.
test := &pkg.tests[len(pkg.tests)-1]
test.addFile(c, er, info)
if !info.isExternalTest {
test.hasInternalTest = true
}
default:
pkg.hasMainFunction = pkg.hasMainFunction || info.hasMainFunction
pkg.library.addFile(c, er, info)
}
return nil
}
// isCommand returns true if the package name is "main".
func (pkg *goPackage) isCommand() bool {
return pkg.name == "main" && pkg.hasMainFunction
}
// 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 (pkg *goPackage) isBuildable(c *config.Config) bool {
return pkg.firstGoFile() != "" || !pkg.proto.sources.isEmpty()
}
// firstGoFile returns the name of a .go file if the package contains at least
// one .go file, or "" otherwise.
func (pkg *goPackage) firstGoFile() string {
goSrcs := []platformStringsBuilder{
pkg.library.sources,
pkg.binary.sources,
}
for _, test := range pkg.tests {
goSrcs = append(goSrcs, test.sources)
}
for _, sb := range goSrcs {
if sb.strs != nil {
for s := range sb.strs {
if strings.HasSuffix(s, ".go") {
return s
}
}
}
}
return ""
}
func (pkg *goPackage) haveCgo() bool {
if pkg.library.cgo || pkg.binary.cgo {
return true
}
for _, t := range pkg.tests {
if t.cgo {
return true
}
}
return false
}
func (pkg *goPackage) inferImportPath(c *config.Config) error {
if pkg.importPath != "" {
log.Panic("importPath already set")
}
gc := getGoConfig(c)
if !gc.prefixSet {
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)
}
pkg.importPath = InferImportPath(c, pkg.rel)
return nil
}
// libNameFromImportPath returns a a suitable go_library name based on the import path.
// Major version suffixes (eg. "v1") are dropped.
func libNameFromImportPath(dir string) string {
i := strings.LastIndexAny(dir, "/\\")
if i < 0 {
return dir
}
name := dir[i+1:]
if pkgVersionRe.MatchString(name) {
dir := dir[:i]
i = strings.LastIndexAny(dir, "/\\")
if i >= 0 {
name = dir[i+1:]
}
}
return strings.ReplaceAll(name, ".", "_")
}
// libNameByConvention returns a suitable name for a go_library using the given
// naming convention, the import path, and the package name.
func libNameByConvention(nc namingConvention, imp string, pkgName string) string {
if nc == goDefaultLibraryNamingConvention {
return defaultLibName
}
name := libNameFromImportPath(imp)
isCommand := pkgName == "main"
if name == "" {
if isCommand {
name = "lib"
} else {
name = pkgName
}
} else if isCommand {
name += "_lib"
}
return name
}
// testNameByConvention returns a suitable name for a go_test using the given
// naming convention and the import path.
func testNameByConvention(nc namingConvention, imp string) string {
if nc == goDefaultLibraryNamingConvention {
return defaultTestName
}
libName := libNameFromImportPath(imp)
if libName == "" {
libName = "lib"
}
return libName + "_test"
}
// testNameFromSingleSource returns a suitable name for a go_test using the
// single Go source file name.
func testNameFromSingleSource(src string) string {
if i := strings.LastIndexByte(src, '.'); i >= 0 {
src = src[0:i]
}
libName := libNameFromImportPath(src)
if libName == "" {
return ""
}
if strings.HasSuffix(libName, "_test") {
return libName
}
return libName + "_test"
}
// binName returns a suitable name for a go_binary.
func binName(rel, prefix, repoRoot string) string {
return pathtools.RelBaseName(rel, prefix, repoRoot)
}
func InferImportPath(c *config.Config, rel string) string {
gc := getGoConfig(c)
if rel == gc.prefixRel {
return gc.prefix
} else {
fromPrefixRel := strings.TrimPrefix(rel, gc.prefixRel+"/")
return path.Join(gc.prefix, fromPrefixRel)
}
}
func goProtoPackageName(pkg proto.Package) string {
if value, ok := pkg.Options["go_package"]; ok {
if strings.LastIndexByte(value, '/') == -1 {
return value
} else {
if i := strings.LastIndexByte(value, ';'); i != -1 {
return value[i+1:]
} else {
return path.Base(value)
}
}
}
return strings.Replace(pkg.Name, ".", "_", -1)
}
func goProtoImportPath(c *config.Config, pkg proto.Package, rel string) string {
if value, ok := pkg.Options["go_package"]; ok {
if strings.LastIndexByte(value, '/') == -1 {
return InferImportPath(c, rel)
} else if i := strings.LastIndexByte(value, ';'); i != -1 {
return value[:i]
} else {
return value
}
}
return InferImportPath(c, rel)
}
func (t *goTarget) addFile(c *config.Config, er *embedResolver, info fileInfo) {
t.cgo = t.cgo || info.isCgo
add := getPlatformStringsAddFunction(c, info, nil)
add(&t.sources, info.name)
add(&t.imports, info.imports...)
if er != nil {
for _, embed := range info.embeds {
embedSrcs, err := er.resolve(embed)
if err != nil {
log.Print(err)
continue
}
add(&t.embedSrcs, embedSrcs...)
}
}
for _, cppopts := range info.cppopts {
optAdd := add
if !cppopts.empty() {
optAdd = getPlatformStringsAddFunction(c, info, cppopts)
}
optAdd(&t.cppopts, cppopts.opts)
}
for _, copts := range info.copts {
optAdd := add
if !copts.empty() {
optAdd = getPlatformStringsAddFunction(c, info, copts)
}
optAdd(&t.copts, copts.opts)
}
for _, cxxopts := range info.cxxopts {
optAdd := add
if !cxxopts.empty() {
optAdd = getPlatformStringsAddFunction(c, info, cxxopts)
}
optAdd(&t.cxxopts, cxxopts.opts)
}
for _, clinkopts := range info.clinkopts {
optAdd := add
if !clinkopts.empty() {
optAdd = getPlatformStringsAddFunction(c, info, clinkopts)
}
optAdd(&t.clinkopts, clinkopts.opts)
}
}
func protoTargetFromProtoPackage(name string, pkg proto.Package) protoTarget {
target := protoTarget{name: name}
for _, fInfo := range pkg.Files {
target.sources.addGenericString(fInfo.Path)
}
for i := range pkg.Imports {
target.imports.addGenericString(i)
}
target.hasServices = pkg.HasServices
return target
}
func (t *protoTarget) addFile(info fileInfo) {
t.sources.addGenericString(info.name)
for _, imp := range info.imports {
t.imports.addGenericString(imp)
}
t.hasServices = t.hasServices || info.hasServices
}
// 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 *cgoTagsAndOpts) func(sb *platformStringsBuilder, ss ...string) {
isOSSpecific, isArchSpecific := isOSArchSpecific(info, cgoTags)
v := getGoConfig(c).rulesGoVersion
constraintPrefix := "@" + getGoConfig(c).rulesGoRepoName + "//go/platform:"
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 rule.KnownOSs {
if rulesGoSupportsOS(v, os) &&
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, constraintPrefix)
}
}
}
case !isOSSpecific && isArchSpecific:
var archMatch []string
for _, arch := range rule.KnownArchs {
if rulesGoSupportsArch(v, arch) &&
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, constraintPrefix)
}
}
}
default:
var platformMatch []rule.Platform
for _, platform := range rule.KnownPlatforms {
if rulesGoSupportsPlatform(v, platform) &&
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, constraintPrefix)
}
}
}
}
return func(_ *platformStringsBuilder, _ ...string) {}
}
func (sb *platformStringsBuilder) isEmpty() bool {
return sb.strs == nil
}
func (sb *platformStringsBuilder) hasGo() bool {
for s := range sb.strs {
if strings.HasSuffix(s, ".go") {
return true
}
}
return false
}
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, constraintPrefix string) {
if sb.strs == nil {
sb.strs = make(map[string]platformStringInfo)
}
si, ok := sb.strs[s]
if !ok {
si.set = osSet
si.osConstraints = make(map[string]bool)
}
switch si.set {
case genericSet:
return
case osSet:
for _, os := range oss {
si.osConstraints[constraintPrefix+os] = true
}
default:
si.convertToPlatforms(constraintPrefix)
for _, os := range oss {
for _, arch := range rule.KnownOSArchs[os] {
si.platformConstraints[rule.PlatformConstraint{
Platform: rule.Platform{OS: os, Arch: arch},
ConstraintPrefix: constraintPrefix,
}] = true
}
}
}
sb.strs[s] = si
}
func (sb *platformStringsBuilder) addArchString(s string, archs []string, constraintPrefix string) {
if sb.strs == nil {
sb.strs = make(map[string]platformStringInfo)
}
si, ok := sb.strs[s]
if !ok {
si.set = archSet
si.archConstraints = make(map[string]bool)
}
switch si.set {
case genericSet:
return
case archSet:
for _, arch := range archs {
si.archConstraints[constraintPrefix+arch] = true
}
default:
si.convertToPlatforms(constraintPrefix)
for _, arch := range archs {
for _, os := range rule.KnownArchOSs[arch] {
si.platformConstraints[rule.PlatformConstraint{
Platform: rule.Platform{OS: os, Arch: arch},
ConstraintPrefix: constraintPrefix,
}] = true
}
}
}
sb.strs[s] = si
}
func (sb *platformStringsBuilder) addPlatformString(s string, platforms []rule.Platform, constraintPrefix string) {
if sb.strs == nil {
sb.strs = make(map[string]platformStringInfo)
}
si, ok := sb.strs[s]
if !ok {
si.set = platformSet
si.platformConstraints = make(map[rule.PlatformConstraint]bool)
}
switch si.set {
case genericSet:
return
default:
si.convertToPlatforms(constraintPrefix)
for _, p := range platforms {
pConstraint := rule.PlatformConstraint{Platform: rule.Platform{OS: p.OS, Arch: p.Arch}, ConstraintPrefix: constraintPrefix}
si.platformConstraints[pConstraint] = true
}
}
sb.strs[s] = si
}
func (sb *platformStringsBuilder) build() rule.PlatformStrings {
var ps rule.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.osConstraints {
ps.OS[os] = append(ps.OS[os], s)
}
case archSet:
if ps.Arch == nil {
ps.Arch = make(map[string][]string)
}
for arch := range si.archConstraints {
ps.Arch[arch] = append(ps.Arch[arch], s)
}
case platformSet:
if ps.Platform == nil {
ps.Platform = make(map[rule.PlatformConstraint][]string)
}
for p := range si.platformConstraints {
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 (sb *platformStringsBuilder) buildFlat() []string {
strs := make([]string, 0, len(sb.strs))
for s := range sb.strs {
strs = append(strs, s)
}
sort.Strings(strs)
return strs
}
func (si *platformStringInfo) convertToPlatforms(constraintPrefix string) {
switch si.set {
case genericSet:
log.Panic("cannot convert generic string to platformConstraints")
case platformSet:
return
case osSet:
si.set = platformSet
si.platformConstraints = make(map[rule.PlatformConstraint]bool)
for osConstraint := range si.osConstraints {
os := strings.TrimPrefix(osConstraint, constraintPrefix)
for _, arch := range rule.KnownOSArchs[os] {
si.platformConstraints[rule.PlatformConstraint{
Platform: rule.Platform{OS: os, Arch: arch},
ConstraintPrefix: constraintPrefix,
}] = true
}
}
si.osConstraints = nil
case archSet:
si.set = platformSet
si.platformConstraints = make(map[rule.PlatformConstraint]bool)
for archConstraint := range si.archConstraints {
arch := strings.TrimPrefix(archConstraint, constraintPrefix)
for _, os := range rule.KnownArchOSs[arch] {
si.platformConstraints[rule.PlatformConstraint{
Platform: rule.Platform{OS: os, Arch: arch},
ConstraintPrefix: constraintPrefix,
}] = true
}
}
si.archConstraints = nil
}
}
var semverRex = regexp.MustCompile(`^.*?(/v\d+)(?:/.*)?$`)
// pathWithoutSemver removes a semantic version suffix from path.
// For example, if path is "example.com/foo/v2/bar", pathWithoutSemver
// will return "example.com/foo/bar". If there is no semantic version suffix,
// "" will be returned.
func pathWithoutSemver(path string) string {
m := semverRex.FindStringSubmatchIndex(path)
if m == nil {
return ""
}
v := path[m[2]+2 : m[3]]
if v[0] == '0' || v == "1" {
return ""
}
return path[:m[2]] + path[m[3]:]
}