| /* Copyright 2018 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 ( |
| "errors" |
| "flag" |
| "fmt" |
| "go/build" |
| "log" |
| "os" |
| "path" |
| "path/filepath" |
| "regexp" |
| "strconv" |
| "strings" |
| |
| "github.com/bazelbuild/bazel-gazelle/config" |
| gzflag "github.com/bazelbuild/bazel-gazelle/flag" |
| "github.com/bazelbuild/bazel-gazelle/internal/module" |
| "github.com/bazelbuild/bazel-gazelle/internal/version" |
| "github.com/bazelbuild/bazel-gazelle/language/proto" |
| "github.com/bazelbuild/bazel-gazelle/repo" |
| "github.com/bazelbuild/bazel-gazelle/rule" |
| bzl "github.com/bazelbuild/buildtools/build" |
| "golang.org/x/mod/modfile" |
| ) |
| |
| var minimumRulesGoVersion = version.Version{0, 29, 0} |
| |
| // goConfig contains configuration values related to Go rules. |
| type goConfig struct { |
| // The name under which the rules_go repository can be referenced from the |
| // repository in which Gazelle is running. |
| rulesGoRepoName string |
| |
| // rulesGoVersion is the version of io_bazel_rules_go being used. Determined |
| // by reading go/def.bzl. May be unset if the version can't be read. |
| rulesGoVersion version.Version |
| |
| // genericTags is a set of tags that Gazelle considers to be true. Set with |
| // -build_tags or # gazelle:build_tags. Some tags, like gc, are always on. |
| genericTags map[string]bool |
| |
| // prefix is a prefix of an import path, used to generate importpath |
| // attributes. Set with -go_prefix or # gazelle:prefix. |
| prefix string |
| |
| // prefixRel is the package name of the directory where the prefix was set |
| // ("" for the root directory). |
| prefixRel string |
| |
| // prefixSet indicates whether the prefix was set explicitly. It is an error |
| // to infer an importpath for a rule without setting the prefix. |
| prefixSet bool |
| |
| // importMapPrefix is a prefix of a package path, used to generate importmap |
| // attributes. Set with # gazelle:importmap_prefix. |
| importMapPrefix string |
| |
| // importMapPrefixRel is the package name of the directory where importMapPrefix |
| // was set ("" for the root directory). |
| importMapPrefixRel string |
| |
| // depMode determines how imports that are not standard, indexed, or local |
| // (under the current prefix) should be resolved. |
| depMode dependencyMode |
| |
| // goGenerateProto indicates whether to generate go_proto_library |
| goGenerateProto bool |
| |
| // goNamingConvention controls the name of generated targets |
| goNamingConvention namingConvention |
| |
| // goNamingConventionExternal controls the default naming convention for |
| // imports in external repositories with unknown naming conventions. |
| goNamingConventionExternal namingConvention |
| |
| // goProtoCompilers is the protocol buffers compiler(s) to use for go code. |
| goProtoCompilers []string |
| |
| // goProtoCompilersSet indicates whether goProtoCompiler was set explicitly. |
| goProtoCompilersSet bool |
| |
| // goGrpcCompilers is the gRPC compiler(s) to use for go code. |
| goGrpcCompilers []string |
| |
| // goGrpcCompilersSet indicates whether goGrpcCompiler was set explicitly. |
| goGrpcCompilersSet bool |
| |
| // goRepositoryMode is true if Gazelle was invoked by a go_repository rule. |
| // In this mode, we won't go out to the network to resolve external deps. |
| goRepositoryMode bool |
| |
| // By default, internal packages are only visible to its siblings. |
| // goVisibility adds a list of packages the internal packages should be |
| // visible to |
| goVisibility []string |
| |
| // moduleMode is true if the current directory is intended to be built |
| // as part of a module. Minimal module compatibility won't be supported |
| // if this is true in the root directory. External dependencies may be |
| // resolved differently (also depending on goRepositoryMode). |
| moduleMode bool |
| |
| // map between external repo names and their `build_naming_convention` |
| // attribute. |
| repoNamingConvention map[string]namingConvention |
| |
| // submodules is a list of modules which have the current module's path |
| // as a prefix of their own path. This affects visibility attributes |
| // in internal packages. |
| submodules []moduleRepo |
| |
| // testMode determines how go_test targets are generated. |
| testMode testMode |
| |
| // buildDirectives, buildExternalAttr, buildExtraArgsAttr, |
| // buildFileGenerationAttr, buildFileNamesAttr, buildFileProtoModeAttr and |
| // buildTagsAttr are attributes for go_repository rules, set on the command |
| // line. |
| buildDirectivesAttr, buildExternalAttr, buildExtraArgsAttr, buildFileGenerationAttr, buildFileNamesAttr, buildFileProtoModeAttr, buildTagsAttr string |
| |
| // goSearch is a list of additional directories that may contain Go libraries. |
| // Subdirectories within these roots may be indexed when lazy indexing |
| // is enabled. Each directory has an associated prefix, specified as part |
| // of the go_search directive. For example, if there's a directive |
| // '# gazelle:go_search replace/b example.com/b', and Gazelle sees an |
| // import of 'example.com/b/p', Gazelle indexes 'replace/b/p'. |
| goSearch []goSearch |
| } |
| |
| // testMode determines how go_test rules are generated. |
| type testMode int |
| |
| const ( |
| // defaultTestMode generates a go_test for the primary package in a directory. |
| defaultTestMode = iota |
| |
| // fileTestMode generates a go_test for each Go test file. |
| fileTestMode |
| ) |
| |
| var ( |
| defaultGoProtoCompilers = []string{"@io_bazel_rules_go//proto:go_proto"} |
| defaultGoGrpcCompilers = []string{"@io_bazel_rules_go//proto:go_grpc_v2"} |
| ) |
| |
| func (m testMode) String() string { |
| switch m { |
| case defaultTestMode: |
| return "default" |
| case fileTestMode: |
| return "file" |
| default: |
| return "unknown" |
| } |
| } |
| |
| func testModeFromString(s string) (testMode, error) { |
| switch s { |
| case "default": |
| return defaultTestMode, nil |
| case "file": |
| return fileTestMode, nil |
| default: |
| return 0, fmt.Errorf("unrecognized go_test mode: %q", s) |
| } |
| } |
| |
| func newGoConfig() *goConfig { |
| gc := &goConfig{ |
| goProtoCompilers: defaultGoProtoCompilers, |
| goGrpcCompilers: defaultGoGrpcCompilers, |
| goGenerateProto: true, |
| } |
| if gc.genericTags == nil { |
| gc.genericTags = make(map[string]bool) |
| } |
| // Add default tags |
| gc.genericTags["gc"] = true |
| return gc |
| } |
| |
| func getGoConfig(c *config.Config) *goConfig { |
| return c.Exts[goName].(*goConfig) |
| } |
| |
| func (gc *goConfig) clone() *goConfig { |
| gcCopy := *gc |
| gcCopy.genericTags = make(map[string]bool) |
| for k, v := range gc.genericTags { |
| gcCopy.genericTags[k] = v |
| } |
| gcCopy.goProtoCompilers = gc.goProtoCompilers[:len(gc.goProtoCompilers):len(gc.goProtoCompilers)] |
| gcCopy.goGrpcCompilers = gc.goGrpcCompilers[:len(gc.goGrpcCompilers):len(gc.goGrpcCompilers)] |
| gcCopy.submodules = gc.submodules[:len(gc.submodules):len(gc.submodules)] |
| gcCopy.goSearch = gc.goSearch[:len(gc.goSearch):len(gc.goSearch)] |
| return &gcCopy |
| } |
| |
| // setBuildTags sets genericTags by parsing as a comma separated list. An |
| // error will be returned for tags that wouldn't be recognized by "go build". |
| func (gc *goConfig) setBuildTags(tags string) error { |
| if tags == "" { |
| return nil |
| } |
| for _, t := range strings.Split(tags, ",") { |
| if strings.HasPrefix(t, "!") { |
| return fmt.Errorf("build tags can't be negated: %s", t) |
| } |
| gc.genericTags[t] = true |
| } |
| return nil |
| } |
| |
| func getProtoMode(c *config.Config) proto.Mode { |
| if gc := getGoConfig(c); !gc.goGenerateProto { |
| return proto.DisableMode |
| } else if pc := proto.GetProtoConfig(c); pc != nil { |
| return pc.Mode |
| } else { |
| return proto.DisableGlobalMode |
| } |
| } |
| |
| // dependencyMode determines how imports of packages outside of the prefix |
| // are resolved. |
| type dependencyMode int |
| |
| const ( |
| // externalMode indicates imports should be resolved to external dependencies |
| // (declared in WORKSPACE). Calls out to the network if an import can't be resolved |
| // locally. |
| externalMode dependencyMode = iota |
| |
| // staticMode indicates imports should be resolved only to dependencies known by |
| // Gazelle (declared in WORKSPACE). Unknown imports are ignored. |
| staticMode |
| |
| // vendorMode indicates imports should be resolved to libraries in the |
| // vendor directory. |
| vendorMode |
| ) |
| |
| func (m dependencyMode) String() string { |
| switch m { |
| case externalMode: |
| return "external" |
| case staticMode: |
| return "static" |
| case vendorMode: |
| return "vendor" |
| } |
| return "" |
| } |
| |
| type externalFlag struct { |
| depMode *dependencyMode |
| } |
| |
| func (f *externalFlag) Set(value string) error { |
| switch value { |
| case "external": |
| *f.depMode = externalMode |
| case "static": |
| *f.depMode = staticMode |
| case "vendored": |
| *f.depMode = vendorMode |
| default: |
| return fmt.Errorf("unrecognized dependency mode: %q", value) |
| } |
| return nil |
| } |
| |
| func (f *externalFlag) String() string { |
| if f == nil || f.depMode == nil { |
| return "external" |
| } |
| return f.depMode.String() |
| } |
| |
| type tagsFlag func(string) error |
| |
| func (f tagsFlag) Set(value string) error { |
| return f(value) |
| } |
| |
| func (f tagsFlag) String() string { |
| return "" |
| } |
| |
| type namingConventionFlag struct { |
| nc *namingConvention |
| } |
| |
| func (f namingConventionFlag) Set(value string) error { |
| if nc, err := namingConventionFromString(value); err != nil { |
| return err |
| } else { |
| *f.nc = nc |
| return nil |
| } |
| } |
| |
| func (f *namingConventionFlag) String() string { |
| if f == nil || f.nc == nil { |
| return "naming_convention" |
| } |
| return f.nc.String() |
| } |
| |
| // namingConvention determines how go targets are named. |
| type namingConvention int |
| |
| const ( |
| // Try to detect the naming convention in use. |
| unknownNamingConvention namingConvention = iota |
| |
| // 'go_default_library' and 'go_default_test' |
| goDefaultLibraryNamingConvention |
| |
| // For an import path that ends with foo, the go_library rules target is |
| // named 'foo', the go_test is named 'foo_test'. |
| // For a main package, the go_binary takes the 'foo' name, the library |
| // is named 'foo_lib', and the go_test is named 'foo_test'. |
| importNamingConvention |
| |
| // Same as importNamingConvention, but generate alias rules for libraries that have |
| // the legacy 'go_default_library' name. |
| importAliasNamingConvention |
| ) |
| |
| func (nc namingConvention) String() string { |
| switch nc { |
| case goDefaultLibraryNamingConvention: |
| return "go_default_library" |
| case importNamingConvention: |
| return "import" |
| case importAliasNamingConvention: |
| return "import_alias" |
| } |
| return "" |
| } |
| |
| func namingConventionFromString(s string) (namingConvention, error) { |
| switch s { |
| case "": |
| return unknownNamingConvention, nil |
| case "go_default_library": |
| return goDefaultLibraryNamingConvention, nil |
| case "import": |
| return importNamingConvention, nil |
| case "import_alias": |
| return importAliasNamingConvention, nil |
| default: |
| return unknownNamingConvention, fmt.Errorf("unknown naming convention %q", s) |
| } |
| } |
| |
| type moduleRepo struct { |
| repoName, modulePath string |
| } |
| |
| type goSearch struct { |
| rel, prefix string |
| } |
| |
| var ( |
| validBuildExternalAttr = []string{"external", "vendored"} |
| validBuildFileGenerationAttr = []string{"auto", "on", "off", "clean"} |
| validBuildFileProtoModeAttr = []string{"default", "legacy", "disable", "disable_global", "package"} |
| ) |
| |
| func (*goLang) KnownDirectives() []string { |
| return []string{ |
| "build_tags", |
| "go_generate_proto", |
| "go_grpc_compilers", |
| "go_naming_convention", |
| "go_naming_convention_external", |
| "go_proto_compilers", |
| "go_search", |
| "go_test", |
| "go_visibility", |
| "importmap_prefix", |
| "prefix", |
| } |
| } |
| |
| func (*goLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) { |
| gc := newGoConfig() |
| switch cmd { |
| case "fix", "update": |
| fs.Var( |
| tagsFlag(gc.setBuildTags), |
| "build_tags", |
| "comma-separated list of build tags. If not specified, Gazelle will not\n\tfilter sources with build constraints.") |
| fs.Var( |
| &gzflag.ExplicitFlag{Value: &gc.prefix, IsSet: &gc.prefixSet}, |
| "go_prefix", |
| "prefix of import paths in the current workspace") |
| fs.Var( |
| &externalFlag{&gc.depMode}, |
| "external", |
| "external: resolve external packages with go_repository\n\tvendored: resolve external packages as packages in vendor/") |
| fs.Var( |
| &gzflag.MultiFlag{Values: &gc.goProtoCompilers, IsSet: &gc.goProtoCompilersSet}, |
| "go_proto_compiler", |
| "go_proto_library compiler to use (may be repeated)") |
| fs.Var( |
| &gzflag.MultiFlag{Values: &gc.goGrpcCompilers, IsSet: &gc.goGrpcCompilersSet}, |
| "go_grpc_compiler", |
| "go_proto_library compiler to use for gRPC (may be repeated)") |
| fs.BoolVar( |
| &gc.goRepositoryMode, |
| "go_repository_mode", |
| false, |
| "set when gazelle is invoked by go_repository") |
| fs.BoolVar( |
| &gc.moduleMode, |
| "go_repository_module_mode", |
| false, |
| "set when gazelle is invoked by go_repository in module mode") |
| fs.Var( |
| &namingConventionFlag{&gc.goNamingConvention}, |
| "go_naming_convention", |
| "controls generated library names. One of (go_default_library, import, import_alias)") |
| fs.Var( |
| &namingConventionFlag{&gc.goNamingConventionExternal}, |
| "go_naming_convention_external", |
| "controls naming convention used when resolving libraries in external repositories with unknown conventions") |
| |
| case "update-repos": |
| fs.StringVar(&gc.buildDirectivesAttr, |
| "build_directives", |
| "", |
| "Sets the build_directives attribute for the generated go_repository rule(s).") |
| fs.Var(&gzflag.AllowedStringFlag{Value: &gc.buildExternalAttr, Allowed: validBuildExternalAttr}, |
| "build_external", |
| "Sets the build_external attribute for the generated go_repository rule(s).") |
| fs.StringVar(&gc.buildExtraArgsAttr, |
| "build_extra_args", |
| "", |
| "Sets the build_extra_args attribute for the generated go_repository rule(s).") |
| fs.Var(&gzflag.AllowedStringFlag{Value: &gc.buildFileGenerationAttr, Allowed: validBuildFileGenerationAttr}, |
| "build_file_generation", |
| "Sets the build_file_generation attribute for the generated go_repository rule(s).") |
| fs.StringVar(&gc.buildFileNamesAttr, |
| "build_file_names", |
| "", |
| "Sets the build_file_name attribute for the generated go_repository rule(s).") |
| fs.Var(&gzflag.AllowedStringFlag{Value: &gc.buildFileProtoModeAttr, Allowed: validBuildFileProtoModeAttr}, |
| "build_file_proto_mode", |
| "Sets the build_file_proto_mode attribute for the generated go_repository rule(s).") |
| fs.StringVar(&gc.buildTagsAttr, |
| "build_tags", |
| "", |
| "Sets the build_tags attribute for the generated go_repository rule(s).") |
| } |
| c.Exts[goName] = gc |
| } |
| |
| func (*goLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { |
| // The base of the -go_prefix flag may be used to generate proto_library |
| // rule names when there are no .proto sources (empty rules to be deleted) |
| // or when the package name can't be determined. |
| // TODO(jayconrod): deprecate and remove this behavior. |
| gc := getGoConfig(c) |
| if pc := proto.GetProtoConfig(c); pc != nil { |
| pc.GoPrefix = gc.prefix |
| } |
| |
| // List modules that may refer to internal packages in this module. |
| for _, r := range c.Repos { |
| if r.Kind() != "go_repository" { |
| continue |
| } |
| modulePath := r.AttrString("importpath") |
| if !strings.HasPrefix(modulePath, gc.prefix+"/") { |
| continue |
| } |
| m := moduleRepo{ |
| repoName: r.Name(), |
| modulePath: modulePath, |
| } |
| gc.submodules = append(gc.submodules, m) |
| } |
| |
| return nil |
| } |
| |
| func (*goLang) Configure(c *config.Config, rel string, f *rule.File) { |
| var gc *goConfig |
| if raw, ok := c.Exts[goName]; !ok { |
| gc = newGoConfig() |
| } else { |
| gc = raw.(*goConfig).clone() |
| } |
| c.Exts[goName] = gc |
| |
| if rel == "" { |
| moduleToApparentName, err := module.ExtractModuleToApparentNameMapping(c.RepoRoot) |
| if err != nil { |
| log.Print(err) |
| } else { |
| gc.rulesGoRepoName = moduleToApparentName("rules_go") |
| } |
| if gc.rulesGoRepoName == "" { |
| // The legacy name used in WORKSPACE. |
| gc.rulesGoRepoName = "io_bazel_rules_go" |
| } |
| |
| const message = `Gazelle may not be compatible with this version of rules_go. |
| Update io_bazel_rules_go to a newer version in your WORKSPACE file.` |
| gc.rulesGoVersion, err = findRulesGoVersion(c) |
| if c.ShouldFix { |
| // Only check the version when "fix" is run. Generated build files |
| // frequently work with older version of rules_go, and we don't want to |
| // nag too much since there's no way to disable this warning. |
| // Also, don't print a warning if the rules_go repo hasn't been fetched, |
| // since that's a common issue when Gazelle is run as a separate binary. |
| if err != nil && err != errRulesGoRepoNotFound && c.ShouldFix { |
| log.Printf("%v\n%s", err, message) |
| } else if err == nil && gc.rulesGoVersion.Compare(minimumRulesGoVersion) < 0 { |
| log.Printf("Found RULES_GO_VERSION %s. Minimum compatible version is %s.\n%s", gc.rulesGoVersion, minimumRulesGoVersion, message) |
| } |
| } |
| repoNamingConvention := map[string]namingConvention{} |
| for _, repo := range c.Repos { |
| if repo.Kind() == "go_repository" { |
| var name string |
| if apparentName := c.ModuleToApparentName(repo.AttrString("module_name")); apparentName != "" { |
| name = apparentName |
| } else { |
| name = repo.Name() |
| } |
| if attr := repo.AttrString("build_naming_convention"); attr == "" { |
| // No naming convention specified. |
| // go_repsitory uses importAliasNamingConvention by default, so we |
| // could use whichever name. |
| // resolveExternal should take that as a signal to follow the current |
| // naming convention to avoid churn. |
| repoNamingConvention[name] = importAliasNamingConvention |
| } else if nc, err := namingConventionFromString(attr); err != nil { |
| log.Printf("in go_repository named %q: %v", name, err) |
| } else { |
| repoNamingConvention[name] = nc |
| } |
| } |
| } |
| gc.repoNamingConvention = repoNamingConvention |
| } |
| |
| if !gc.moduleMode { |
| st, err := os.Stat(filepath.Join(c.RepoRoot, filepath.FromSlash(rel), "go.mod")) |
| if err == nil && !st.IsDir() { |
| gc.moduleMode = true |
| } |
| } |
| |
| if path.Base(rel) == "vendor" { |
| gc.importMapPrefix = InferImportPath(c, rel) |
| gc.importMapPrefixRel = rel |
| gc.prefix = "" |
| gc.prefixRel = rel |
| } |
| |
| if f != nil { |
| setPrefix := func(prefix string) { |
| if err := checkPrefix(prefix); err != nil { |
| log.Print(err) |
| return |
| } |
| gc.prefix = prefix |
| gc.prefixSet = true |
| gc.prefixRel = rel |
| gc.goSearch = append(gc.goSearch, goSearch{rel: rel, prefix: prefix}) |
| } |
| for _, d := range f.Directives { |
| switch d.Key { |
| case "build_tags": |
| if err := gc.setBuildTags(d.Value); err != nil { |
| log.Print(err) |
| continue |
| } |
| |
| case "go_generate_proto": |
| if goGenerateProto, err := strconv.ParseBool(d.Value); err == nil { |
| gc.goGenerateProto = goGenerateProto |
| } else { |
| log.Printf("parsing go_generate_proto: %v", err) |
| } |
| |
| case "go_naming_convention": |
| if nc, err := namingConventionFromString(d.Value); err == nil { |
| gc.goNamingConvention = nc |
| } else { |
| log.Print(err) |
| } |
| |
| case "go_naming_convention_external": |
| if nc, err := namingConventionFromString(d.Value); err == nil { |
| gc.goNamingConventionExternal = nc |
| } else { |
| log.Print(err) |
| } |
| |
| case "go_grpc_compilers": |
| // Special syntax (empty value) to reset directive. |
| if d.Value == "" { |
| gc.goGrpcCompilersSet = false |
| gc.goGrpcCompilers = defaultGoGrpcCompilers |
| } else { |
| gc.goGrpcCompilersSet = true |
| gc.goGrpcCompilers = splitValue(d.Value) |
| } |
| |
| case "go_proto_compilers": |
| // Special syntax (empty value) to reset directive. |
| if d.Value == "" { |
| gc.goProtoCompilersSet = false |
| gc.goProtoCompilers = defaultGoProtoCompilers |
| } else { |
| gc.goProtoCompilersSet = true |
| gc.goProtoCompilers = splitValue(d.Value) |
| } |
| |
| case "go_search": |
| // Special syntax (empty value) to reset directive. |
| if d.Value == "" { |
| gc.goSearch = nil |
| } else { |
| args, err := splitQuoted(d.Value) |
| if err != nil { |
| log.Print(err) |
| continue |
| } |
| if len(args) == 0 || len(args) > 2 { |
| log.Printf("# gazelle:go_search: got %d arguments, expected 1 or 2, a relative directory path and a go prefix", len(args)) |
| continue |
| } |
| searchDir := args[0] |
| prefix := "" |
| if len(args) > 1 { |
| prefix = args[1] |
| } |
| searchRel := path.Join(rel, searchDir) |
| if searchRel == "." { |
| searchRel = "" |
| } |
| gc.goSearch = append(gc.goSearch, goSearch{rel: searchRel, prefix: prefix}) |
| } |
| |
| case "go_test": |
| mode, err := testModeFromString(d.Value) |
| if err != nil { |
| log.Print(err) |
| continue |
| } |
| gc.testMode = mode |
| |
| case "go_visibility": |
| gc.goVisibility = append(gc.goVisibility, strings.TrimSpace(d.Value)) |
| |
| case "importmap_prefix": |
| gc.importMapPrefix = d.Value |
| gc.importMapPrefixRel = rel |
| |
| case "prefix": |
| setPrefix(d.Value) |
| } |
| } |
| |
| if !gc.prefixSet { |
| for _, r := range f.Rules { |
| switch r.Kind() { |
| case "go_prefix": |
| args := r.Args() |
| if len(args) != 1 { |
| continue |
| } |
| s, ok := args[0].(*bzl.StringExpr) |
| if !ok { |
| continue |
| } |
| setPrefix(s.Value) |
| |
| case "gazelle": |
| if prefix := r.AttrString("prefix"); prefix != "" { |
| setPrefix(prefix) |
| } |
| } |
| } |
| } |
| if !gc.prefixSet { |
| // Parse the module directive out of the go.mod file, if present. |
| goModPath := filepath.Join(c.RepoRoot, filepath.FromSlash(rel), "go.mod") |
| goMod, err := os.ReadFile(goModPath) |
| // Reading the go.mod file is best-effort and may fail for various reasons, such as |
| // the file not existing or being a directory. Do not report errors. |
| if err == nil { |
| goModFile, err := modfile.ParseLax(goModPath, goMod, nil) |
| // If the go.mod file exists but is malformed, report the error. |
| if err != nil { |
| log.Printf("parsing %s: %s", goModPath, err) |
| } else { |
| setPrefix(goModFile.Module.Mod.Path) |
| } |
| } |
| } |
| } |
| |
| if gc.goNamingConvention == unknownNamingConvention { |
| gc.goNamingConvention = detectNamingConvention(c, f) |
| } |
| } |
| |
| // checkPrefix checks that a string may be used as a prefix. We forbid local |
| // (relative) imports and those beginning with "/". We allow the empty string, |
| // but generated rules must not have an empty importpath. |
| func checkPrefix(prefix string) error { |
| if strings.HasPrefix(prefix, "/") || build.IsLocalImport(prefix) { |
| return fmt.Errorf("invalid prefix: %q", prefix) |
| } |
| return nil |
| } |
| |
| // splitDirective splits a comma-separated directive value into its component |
| // parts, trimming each of any whitespace characters. |
| func splitValue(value string) []string { |
| parts := strings.Split(value, ",") |
| values := make([]string, 0, len(parts)) |
| for _, part := range parts { |
| values = append(values, strings.TrimSpace(part)) |
| } |
| return values |
| } |
| |
| // findRulesGoVersion attempts to infer the version of io_bazel_rules_go. |
| // It can read the external directory (if bazel has fetched it), or it can |
| // read WORKSPACE. Neither method is completely reliable. |
| func findRulesGoVersion(c *config.Config) (version.Version, error) { |
| const message = `Gazelle may not be compatible with this version of rules_go. |
| Update io_bazel_rules_go to a newer version in your WORKSPACE file.` |
| |
| var vstr string |
| if rulesGoPath, err := repo.FindExternalRepo(c.RepoRoot, config.RulesGoRepoName); err == nil { |
| // Bazel has already fetched io_bazel_rules_go. We can read its version |
| // from //go:def.bzl. |
| defBzlPath := filepath.Join(rulesGoPath, "go", "def.bzl") |
| defBzlContent, err := os.ReadFile(defBzlPath) |
| if err != nil { |
| return nil, err |
| } |
| versionRe := regexp.MustCompile(`(?m)^RULES_GO_VERSION = ['"]([0-9.]*)['"]`) |
| match := versionRe.FindSubmatch(defBzlContent) |
| if match == nil { |
| return nil, fmt.Errorf("RULES_GO_VERSION not found in @%s//go:def.bzl.\n%s", config.RulesGoRepoName, message) |
| } |
| vstr = string(match[1]) |
| } else { |
| // Bazel has not fetched io_bazel_rules_go. Maybe we can find it in the |
| // WORKSPACE file. |
| re := regexp.MustCompile(`github\.com/bazelbuild/rules_go/releases/download/v([0-9.]+)/`) |
| RepoLoop: |
| for _, r := range c.Repos { |
| if r.Kind() == "http_archive" && r.Name() == "io_bazel_rules_go" { |
| for _, u := range r.AttrStrings("urls") { |
| if m := re.FindStringSubmatch(u); m != nil { |
| vstr = m[1] |
| break RepoLoop |
| } |
| } |
| } |
| } |
| } |
| |
| if vstr == "" { |
| // Couldn't find a version. We return a specific value since this is not |
| // usually a useful error to report. |
| return nil, errRulesGoRepoNotFound |
| } |
| |
| return version.ParseVersion(vstr) |
| } |
| |
| var errRulesGoRepoNotFound = errors.New(config.RulesGoRepoName + " external repository not found") |
| |
| // detectNamingConvention attempts to detect the naming convention in use by |
| // reading build files in subdirectories of the repository root directory. |
| // |
| // If detectNamingConvention can't detect the naming convention (for example, |
| // because no build files are found or multiple naming conventions are found), |
| // importNamingConvention is returned. |
| func detectNamingConvention(c *config.Config, rootFile *rule.File) namingConvention { |
| if !c.IndexLibraries { |
| // Indexing is disabled, which usually means speed is important and I/O |
| // should be minimized. Let's not open extra files or directories. |
| return importNamingConvention |
| } |
| |
| detectInFile := func(f *rule.File) namingConvention { |
| for _, r := range f.Rules { |
| // NOTE: map_kind is not supported. c.KindMap will not be accurate in |
| // subdirectories. |
| kind := r.Kind() |
| name := r.Name() |
| if kind != "alias" && name == defaultLibName { |
| // Assume any kind of rule with the name "go_default_library" is some |
| // kind of go library. The old version of go_proto_library used this |
| // name, and it's possible with map_kind as well. |
| return goDefaultLibraryNamingConvention |
| } else if isGoLibrary(kind) && name == path.Base(r.AttrString("importpath")) { |
| return importNamingConvention |
| } |
| } |
| return unknownNamingConvention |
| } |
| |
| detectInDir := func(dir, rel string) namingConvention { |
| var f *rule.File |
| for _, name := range c.ValidBuildFileNames { |
| fpath := filepath.Join(dir, name) |
| data, err := os.ReadFile(fpath) |
| if err != nil { |
| continue |
| } |
| f, err = rule.LoadData(fpath, rel, data) |
| if err != nil { |
| continue |
| } |
| } |
| if f == nil { |
| return unknownNamingConvention |
| } |
| return detectInFile(f) |
| } |
| |
| nc := unknownNamingConvention |
| if rootFile != nil { |
| if rootNC := detectInFile(rootFile); rootNC != unknownNamingConvention { |
| return rootNC |
| } |
| } |
| |
| ents, err := os.ReadDir(c.RepoRoot) |
| if err != nil { |
| return importNamingConvention |
| } |
| for _, ent := range ents { |
| if !ent.IsDir() { |
| continue |
| } |
| dirName := ent.Name() |
| dirNC := detectInDir(filepath.Join(c.RepoRoot, dirName), dirName) |
| if dirNC == unknownNamingConvention { |
| continue |
| } |
| if nc != unknownNamingConvention && dirNC != nc { |
| // Subdirectories use different conventions. Return the default. |
| return importNamingConvention |
| } |
| nc = dirNC |
| } |
| if nc == unknownNamingConvention { |
| return importNamingConvention |
| } |
| return nc |
| } |