github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/go/config.go (about) 1 /* Copyright 2018 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 "errors" 20 "flag" 21 "fmt" 22 "go/build" 23 "log" 24 "os" 25 "path" 26 "path/filepath" 27 "regexp" 28 "strconv" 29 "strings" 30 31 "github.com/bazelbuild/bazel-gazelle/config" 32 gzflag "github.com/bazelbuild/bazel-gazelle/flag" 33 "github.com/bazelbuild/bazel-gazelle/internal/module" 34 "github.com/bazelbuild/bazel-gazelle/internal/version" 35 "github.com/bazelbuild/bazel-gazelle/language/proto" 36 "github.com/bazelbuild/bazel-gazelle/repo" 37 "github.com/bazelbuild/bazel-gazelle/rule" 38 bzl "github.com/bazelbuild/buildtools/build" 39 "golang.org/x/mod/modfile" 40 ) 41 42 var minimumRulesGoVersion = version.Version{0, 29, 0} 43 44 // goConfig contains configuration values related to Go rules. 45 type goConfig struct { 46 // The name under which the rules_go repository can be referenced from the 47 // repository in which Gazelle is running. 48 rulesGoRepoName string 49 50 // rulesGoVersion is the version of io_bazel_rules_go being used. Determined 51 // by reading go/def.bzl. May be unset if the version can't be read. 52 rulesGoVersion version.Version 53 54 // genericTags is a set of tags that Gazelle considers to be true. Set with 55 // -build_tags or # gazelle:build_tags. Some tags, like gc, are always on. 56 genericTags map[string]bool 57 58 // prefix is a prefix of an import path, used to generate importpath 59 // attributes. Set with -go_prefix or # gazelle:prefix. 60 prefix string 61 62 // prefixRel is the package name of the directory where the prefix was set 63 // ("" for the root directory). 64 prefixRel string 65 66 // prefixSet indicates whether the prefix was set explicitly. It is an error 67 // to infer an importpath for a rule without setting the prefix. 68 prefixSet bool 69 70 // importMapPrefix is a prefix of a package path, used to generate importmap 71 // attributes. Set with # gazelle:importmap_prefix. 72 importMapPrefix string 73 74 // importMapPrefixRel is the package name of the directory where importMapPrefix 75 // was set ("" for the root directory). 76 importMapPrefixRel string 77 78 // depMode determines how imports that are not standard, indexed, or local 79 // (under the current prefix) should be resolved. 80 depMode dependencyMode 81 82 // goGenerateProto indicates whether to generate go_proto_library 83 goGenerateProto bool 84 85 // goNamingConvention controls the name of generated targets 86 goNamingConvention namingConvention 87 88 // goNamingConventionExternal controls the default naming convention for 89 // imports in external repositories with unknown naming conventions. 90 goNamingConventionExternal namingConvention 91 92 // goProtoCompilers is the protocol buffers compiler(s) to use for go code. 93 goProtoCompilers []string 94 95 // goProtoCompilersSet indicates whether goProtoCompiler was set explicitly. 96 goProtoCompilersSet bool 97 98 // goGrpcCompilers is the gRPC compiler(s) to use for go code. 99 goGrpcCompilers []string 100 101 // goGrpcCompilersSet indicates whether goGrpcCompiler was set explicitly. 102 goGrpcCompilersSet bool 103 104 // goRepositoryMode is true if Gazelle was invoked by a go_repository rule. 105 // In this mode, we won't go out to the network to resolve external deps. 106 goRepositoryMode bool 107 108 // By default, internal packages are only visible to its siblings. 109 // goVisibility adds a list of packages the internal packages should be 110 // visible to 111 goVisibility []string 112 113 // moduleMode is true if the current directory is intended to be built 114 // as part of a module. Minimal module compatibility won't be supported 115 // if this is true in the root directory. External dependencies may be 116 // resolved differently (also depending on goRepositoryMode). 117 moduleMode bool 118 119 // map between external repo names and their `build_naming_convention` 120 // attribute. 121 repoNamingConvention map[string]namingConvention 122 123 // submodules is a list of modules which have the current module's path 124 // as a prefix of their own path. This affects visibility attributes 125 // in internal packages. 126 submodules []moduleRepo 127 128 // testMode determines how go_test targets are generated. 129 testMode testMode 130 131 // buildDirectives, buildExternalAttr, buildExtraArgsAttr, 132 // buildFileGenerationAttr, buildFileNamesAttr, buildFileProtoModeAttr and 133 // buildTagsAttr are attributes for go_repository rules, set on the command 134 // line. 135 buildDirectivesAttr, buildExternalAttr, buildExtraArgsAttr, buildFileGenerationAttr, buildFileNamesAttr, buildFileProtoModeAttr, buildTagsAttr string 136 } 137 138 // testMode determines how go_test rules are generated. 139 type testMode int 140 141 const ( 142 // defaultTestMode generates a go_test for the primary package in a directory. 143 defaultTestMode = iota 144 145 // fileTestMode generates a go_test for each Go test file. 146 fileTestMode 147 ) 148 149 var ( 150 defaultGoProtoCompilers = []string{"@io_bazel_rules_go//proto:go_proto"} 151 defaultGoGrpcCompilers = []string{"@io_bazel_rules_go//proto:go_grpc"} 152 ) 153 154 func (m testMode) String() string { 155 switch m { 156 case defaultTestMode: 157 return "default" 158 case fileTestMode: 159 return "file" 160 default: 161 return "unknown" 162 } 163 } 164 165 func testModeFromString(s string) (testMode, error) { 166 switch s { 167 case "default": 168 return defaultTestMode, nil 169 case "file": 170 return fileTestMode, nil 171 default: 172 return 0, fmt.Errorf("unrecognized go_test mode: %q", s) 173 } 174 } 175 176 func newGoConfig() *goConfig { 177 gc := &goConfig{ 178 goProtoCompilers: defaultGoProtoCompilers, 179 goGrpcCompilers: defaultGoGrpcCompilers, 180 goGenerateProto: true, 181 } 182 gc.preprocessTags() 183 return gc 184 } 185 186 func getGoConfig(c *config.Config) *goConfig { 187 return c.Exts[goName].(*goConfig) 188 } 189 190 func (gc *goConfig) clone() *goConfig { 191 gcCopy := *gc 192 gcCopy.genericTags = make(map[string]bool) 193 for k, v := range gc.genericTags { 194 gcCopy.genericTags[k] = v 195 } 196 gcCopy.goProtoCompilers = gc.goProtoCompilers[:len(gc.goProtoCompilers):len(gc.goProtoCompilers)] 197 gcCopy.goGrpcCompilers = gc.goGrpcCompilers[:len(gc.goGrpcCompilers):len(gc.goGrpcCompilers)] 198 gcCopy.submodules = gc.submodules[:len(gc.submodules):len(gc.submodules)] 199 return &gcCopy 200 } 201 202 // preprocessTags adds some tags which are on by default before they are 203 // used to match files. 204 func (gc *goConfig) preprocessTags() { 205 if gc.genericTags == nil { 206 gc.genericTags = make(map[string]bool) 207 } 208 gc.genericTags["gc"] = true 209 } 210 211 // setBuildTags sets genericTags by parsing as a comma separated list. An 212 // error will be returned for tags that wouldn't be recognized by "go build". 213 // preprocessTags should be called before this. 214 func (gc *goConfig) setBuildTags(tags string) error { 215 if tags == "" { 216 return nil 217 } 218 for _, t := range strings.Split(tags, ",") { 219 if strings.HasPrefix(t, "!") { 220 return fmt.Errorf("build tags can't be negated: %s", t) 221 } 222 gc.genericTags[t] = true 223 } 224 return nil 225 } 226 227 func getProtoMode(c *config.Config) proto.Mode { 228 if gc := getGoConfig(c); !gc.goGenerateProto { 229 return proto.DisableMode 230 } else if pc := proto.GetProtoConfig(c); pc != nil { 231 return pc.Mode 232 } else { 233 return proto.DisableGlobalMode 234 } 235 } 236 237 // dependencyMode determines how imports of packages outside of the prefix 238 // are resolved. 239 type dependencyMode int 240 241 const ( 242 // externalMode indicates imports should be resolved to external dependencies 243 // (declared in WORKSPACE). Calls out to the network if an import can't be resolved 244 // locally. 245 externalMode dependencyMode = iota 246 247 // staticMode indicates imports should be resolved only to dependencies known by 248 // Gazelle (declared in WORKSPACE). Unknown imports are ignored. 249 staticMode 250 251 // vendorMode indicates imports should be resolved to libraries in the 252 // vendor directory. 253 vendorMode 254 ) 255 256 func (m dependencyMode) String() string { 257 switch m { 258 case externalMode: 259 return "external" 260 case staticMode: 261 return "static" 262 case vendorMode: 263 return "vendor" 264 } 265 return "" 266 } 267 268 type externalFlag struct { 269 depMode *dependencyMode 270 } 271 272 func (f *externalFlag) Set(value string) error { 273 switch value { 274 case "external": 275 *f.depMode = externalMode 276 case "static": 277 *f.depMode = staticMode 278 case "vendored": 279 *f.depMode = vendorMode 280 default: 281 return fmt.Errorf("unrecognized dependency mode: %q", value) 282 } 283 return nil 284 } 285 286 func (f *externalFlag) String() string { 287 if f == nil || f.depMode == nil { 288 return "external" 289 } 290 return f.depMode.String() 291 } 292 293 type tagsFlag func(string) error 294 295 func (f tagsFlag) Set(value string) error { 296 return f(value) 297 } 298 299 func (f tagsFlag) String() string { 300 return "" 301 } 302 303 type namingConventionFlag struct { 304 nc *namingConvention 305 } 306 307 func (f namingConventionFlag) Set(value string) error { 308 if nc, err := namingConventionFromString(value); err != nil { 309 return err 310 } else { 311 *f.nc = nc 312 return nil 313 } 314 } 315 316 func (f *namingConventionFlag) String() string { 317 if f == nil || f.nc == nil { 318 return "naming_convention" 319 } 320 return f.nc.String() 321 } 322 323 // namingConvention determines how go targets are named. 324 type namingConvention int 325 326 const ( 327 // Try to detect the naming convention in use. 328 unknownNamingConvention namingConvention = iota 329 330 // 'go_default_library' and 'go_default_test' 331 goDefaultLibraryNamingConvention 332 333 // For an import path that ends with foo, the go_library rules target is 334 // named 'foo', the go_test is named 'foo_test'. 335 // For a main package, the go_binary takes the 'foo' name, the library 336 // is named 'foo_lib', and the go_test is named 'foo_test'. 337 importNamingConvention 338 339 // Same as importNamingConvention, but generate alias rules for libraries that have 340 // the legacy 'go_default_library' name. 341 importAliasNamingConvention 342 ) 343 344 func (nc namingConvention) String() string { 345 switch nc { 346 case goDefaultLibraryNamingConvention: 347 return "go_default_library" 348 case importNamingConvention: 349 return "import" 350 case importAliasNamingConvention: 351 return "import_alias" 352 } 353 return "" 354 } 355 356 func namingConventionFromString(s string) (namingConvention, error) { 357 switch s { 358 case "": 359 return unknownNamingConvention, nil 360 case "go_default_library": 361 return goDefaultLibraryNamingConvention, nil 362 case "import": 363 return importNamingConvention, nil 364 case "import_alias": 365 return importAliasNamingConvention, nil 366 default: 367 return unknownNamingConvention, fmt.Errorf("unknown naming convention %q", s) 368 } 369 } 370 371 type moduleRepo struct { 372 repoName, modulePath string 373 } 374 375 var ( 376 validBuildExternalAttr = []string{"external", "vendored"} 377 validBuildFileGenerationAttr = []string{"auto", "on", "off"} 378 validBuildFileProtoModeAttr = []string{"default", "legacy", "disable", "disable_global", "package"} 379 ) 380 381 func (*goLang) KnownDirectives() []string { 382 return []string{ 383 "build_tags", 384 "go_generate_proto", 385 "go_grpc_compilers", 386 "go_naming_convention", 387 "go_naming_convention_external", 388 "go_proto_compilers", 389 "go_test", 390 "go_visibility", 391 "importmap_prefix", 392 "prefix", 393 } 394 } 395 396 func (*goLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) { 397 gc := newGoConfig() 398 switch cmd { 399 case "fix", "update": 400 fs.Var( 401 tagsFlag(gc.setBuildTags), 402 "build_tags", 403 "comma-separated list of build tags. If not specified, Gazelle will not\n\tfilter sources with build constraints.") 404 fs.Var( 405 &gzflag.ExplicitFlag{Value: &gc.prefix, IsSet: &gc.prefixSet}, 406 "go_prefix", 407 "prefix of import paths in the current workspace") 408 fs.Var( 409 &externalFlag{&gc.depMode}, 410 "external", 411 "external: resolve external packages with go_repository\n\tvendored: resolve external packages as packages in vendor/") 412 fs.Var( 413 &gzflag.MultiFlag{Values: &gc.goProtoCompilers, IsSet: &gc.goProtoCompilersSet}, 414 "go_proto_compiler", 415 "go_proto_library compiler to use (may be repeated)") 416 fs.Var( 417 &gzflag.MultiFlag{Values: &gc.goGrpcCompilers, IsSet: &gc.goGrpcCompilersSet}, 418 "go_grpc_compiler", 419 "go_proto_library compiler to use for gRPC (may be repeated)") 420 fs.BoolVar( 421 &gc.goRepositoryMode, 422 "go_repository_mode", 423 false, 424 "set when gazelle is invoked by go_repository") 425 fs.BoolVar( 426 &gc.moduleMode, 427 "go_repository_module_mode", 428 false, 429 "set when gazelle is invoked by go_repository in module mode") 430 fs.Var( 431 &namingConventionFlag{&gc.goNamingConvention}, 432 "go_naming_convention", 433 "controls generated library names. One of (go_default_library, import, import_alias)") 434 fs.Var( 435 &namingConventionFlag{&gc.goNamingConventionExternal}, 436 "go_naming_convention_external", 437 "controls naming convention used when resolving libraries in external repositories with unknown conventions") 438 439 case "update-repos": 440 fs.StringVar(&gc.buildDirectivesAttr, 441 "build_directives", 442 "", 443 "Sets the build_directives attribute for the generated go_repository rule(s).") 444 fs.Var(&gzflag.AllowedStringFlag{Value: &gc.buildExternalAttr, Allowed: validBuildExternalAttr}, 445 "build_external", 446 "Sets the build_external attribute for the generated go_repository rule(s).") 447 fs.StringVar(&gc.buildExtraArgsAttr, 448 "build_extra_args", 449 "", 450 "Sets the build_extra_args attribute for the generated go_repository rule(s).") 451 fs.Var(&gzflag.AllowedStringFlag{Value: &gc.buildFileGenerationAttr, Allowed: validBuildFileGenerationAttr}, 452 "build_file_generation", 453 "Sets the build_file_generation attribute for the generated go_repository rule(s).") 454 fs.StringVar(&gc.buildFileNamesAttr, 455 "build_file_names", 456 "", 457 "Sets the build_file_name attribute for the generated go_repository rule(s).") 458 fs.Var(&gzflag.AllowedStringFlag{Value: &gc.buildFileProtoModeAttr, Allowed: validBuildFileProtoModeAttr}, 459 "build_file_proto_mode", 460 "Sets the build_file_proto_mode attribute for the generated go_repository rule(s).") 461 fs.StringVar(&gc.buildTagsAttr, 462 "build_tags", 463 "", 464 "Sets the build_tags attribute for the generated go_repository rule(s).") 465 } 466 c.Exts[goName] = gc 467 } 468 469 func (*goLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { 470 // The base of the -go_prefix flag may be used to generate proto_library 471 // rule names when there are no .proto sources (empty rules to be deleted) 472 // or when the package name can't be determined. 473 // TODO(jayconrod): deprecate and remove this behavior. 474 gc := getGoConfig(c) 475 if pc := proto.GetProtoConfig(c); pc != nil { 476 pc.GoPrefix = gc.prefix 477 } 478 479 // List modules that may refer to internal packages in this module. 480 for _, r := range c.Repos { 481 if r.Kind() != "go_repository" { 482 continue 483 } 484 modulePath := r.AttrString("importpath") 485 if !strings.HasPrefix(modulePath, gc.prefix+"/") { 486 continue 487 } 488 m := moduleRepo{ 489 repoName: r.Name(), 490 modulePath: modulePath, 491 } 492 gc.submodules = append(gc.submodules, m) 493 } 494 495 return nil 496 } 497 498 func (*goLang) Configure(c *config.Config, rel string, f *rule.File) { 499 var gc *goConfig 500 if raw, ok := c.Exts[goName]; !ok { 501 gc = newGoConfig() 502 } else { 503 gc = raw.(*goConfig).clone() 504 } 505 c.Exts[goName] = gc 506 507 if rel == "" { 508 moduleToApparentName, err := module.ExtractModuleToApparentNameMapping(c.RepoRoot) 509 if err != nil { 510 log.Print(err) 511 } else { 512 gc.rulesGoRepoName = moduleToApparentName("rules_go") 513 } 514 if gc.rulesGoRepoName == "" { 515 // The legacy name used in WORKSPACE. 516 gc.rulesGoRepoName = "io_bazel_rules_go" 517 } 518 519 const message = `Gazelle may not be compatible with this version of rules_go. 520 Update io_bazel_rules_go to a newer version in your WORKSPACE file.` 521 gc.rulesGoVersion, err = findRulesGoVersion(c) 522 if c.ShouldFix { 523 // Only check the version when "fix" is run. Generated build files 524 // frequently work with older version of rules_go, and we don't want to 525 // nag too much since there's no way to disable this warning. 526 // Also, don't print a warning if the rules_go repo hasn't been fetched, 527 // since that's a common issue when Gazelle is run as a separate binary. 528 if err != nil && err != errRulesGoRepoNotFound && c.ShouldFix { 529 log.Printf("%v\n%s", err, message) 530 } else if err == nil && gc.rulesGoVersion.Compare(minimumRulesGoVersion) < 0 { 531 log.Printf("Found RULES_GO_VERSION %s. Minimum compatible version is %s.\n%s", gc.rulesGoVersion, minimumRulesGoVersion, message) 532 } 533 } 534 repoNamingConvention := map[string]namingConvention{} 535 for _, repo := range c.Repos { 536 if repo.Kind() == "go_repository" { 537 if attr := repo.AttrString("build_naming_convention"); attr == "" { 538 // No naming convention specified. 539 // go_repsitory uses importAliasNamingConvention by default, so we 540 // could use whichever name. 541 // resolveExternal should take that as a signal to follow the current 542 // naming convention to avoid churn. 543 repoNamingConvention[repo.Name()] = importAliasNamingConvention 544 } else if nc, err := namingConventionFromString(attr); err != nil { 545 log.Printf("in go_repository named %q: %v", repo.Name(), err) 546 } else { 547 repoNamingConvention[repo.Name()] = nc 548 } 549 } 550 } 551 gc.repoNamingConvention = repoNamingConvention 552 } 553 554 if !gc.moduleMode { 555 st, err := os.Stat(filepath.Join(c.RepoRoot, filepath.FromSlash(rel), "go.mod")) 556 if err == nil && !st.IsDir() { 557 gc.moduleMode = true 558 } 559 } 560 561 if path.Base(rel) == "vendor" { 562 gc.importMapPrefix = InferImportPath(c, rel) 563 gc.importMapPrefixRel = rel 564 gc.prefix = "" 565 gc.prefixRel = rel 566 } 567 568 if f != nil { 569 setPrefix := func(prefix string) { 570 if err := checkPrefix(prefix); err != nil { 571 log.Print(err) 572 return 573 } 574 gc.prefix = prefix 575 gc.prefixSet = true 576 gc.prefixRel = rel 577 } 578 for _, d := range f.Directives { 579 switch d.Key { 580 case "build_tags": 581 if err := gc.setBuildTags(d.Value); err != nil { 582 log.Print(err) 583 continue 584 } 585 gc.preprocessTags() 586 if err := gc.setBuildTags(d.Value); err != nil { 587 log.Print(err) 588 } 589 590 case "go_generate_proto": 591 if goGenerateProto, err := strconv.ParseBool(d.Value); err == nil { 592 gc.goGenerateProto = goGenerateProto 593 } else { 594 log.Printf("parsing go_generate_proto: %v", err) 595 } 596 597 case "go_naming_convention": 598 if nc, err := namingConventionFromString(d.Value); err == nil { 599 gc.goNamingConvention = nc 600 } else { 601 log.Print(err) 602 } 603 604 case "go_naming_convention_external": 605 if nc, err := namingConventionFromString(d.Value); err == nil { 606 gc.goNamingConventionExternal = nc 607 } else { 608 log.Print(err) 609 } 610 611 case "go_grpc_compilers": 612 // Special syntax (empty value) to reset directive. 613 if d.Value == "" { 614 gc.goGrpcCompilersSet = false 615 gc.goGrpcCompilers = defaultGoGrpcCompilers 616 } else { 617 gc.goGrpcCompilersSet = true 618 gc.goGrpcCompilers = splitValue(d.Value) 619 } 620 621 case "go_proto_compilers": 622 // Special syntax (empty value) to reset directive. 623 if d.Value == "" { 624 gc.goProtoCompilersSet = false 625 gc.goProtoCompilers = defaultGoProtoCompilers 626 } else { 627 gc.goProtoCompilersSet = true 628 gc.goProtoCompilers = splitValue(d.Value) 629 } 630 631 case "go_test": 632 mode, err := testModeFromString(d.Value) 633 if err != nil { 634 log.Print(err) 635 continue 636 } 637 gc.testMode = mode 638 639 case "go_visibility": 640 gc.goVisibility = append(gc.goVisibility, strings.TrimSpace(d.Value)) 641 642 case "importmap_prefix": 643 gc.importMapPrefix = d.Value 644 gc.importMapPrefixRel = rel 645 646 case "prefix": 647 setPrefix(d.Value) 648 } 649 } 650 651 if !gc.prefixSet { 652 for _, r := range f.Rules { 653 switch r.Kind() { 654 case "go_prefix": 655 args := r.Args() 656 if len(args) != 1 { 657 continue 658 } 659 s, ok := args[0].(*bzl.StringExpr) 660 if !ok { 661 continue 662 } 663 setPrefix(s.Value) 664 665 case "gazelle": 666 if prefix := r.AttrString("prefix"); prefix != "" { 667 setPrefix(prefix) 668 } 669 } 670 } 671 } 672 if !gc.prefixSet { 673 // Parse the module directive out of the go.mod file, if present. 674 goModPath := filepath.Join(c.RepoRoot, filepath.FromSlash(rel), "go.mod") 675 goMod, err := os.ReadFile(goModPath) 676 // Reading the go.mod file is best-effort and may fail for various reasons, such as 677 // the file not existing or being a directory. Do not report errors. 678 if err == nil { 679 goModFile, err := modfile.ParseLax(goModPath, goMod, nil) 680 // If the go.mod file exists but is malformed, report the error. 681 if err != nil { 682 log.Printf("parsing %s: %s", goModPath, err) 683 } else { 684 setPrefix(goModFile.Module.Mod.Path) 685 } 686 } 687 } 688 } 689 690 if gc.goNamingConvention == unknownNamingConvention { 691 gc.goNamingConvention = detectNamingConvention(c, f) 692 } 693 } 694 695 // checkPrefix checks that a string may be used as a prefix. We forbid local 696 // (relative) imports and those beginning with "/". We allow the empty string, 697 // but generated rules must not have an empty importpath. 698 func checkPrefix(prefix string) error { 699 if strings.HasPrefix(prefix, "/") || build.IsLocalImport(prefix) { 700 return fmt.Errorf("invalid prefix: %q", prefix) 701 } 702 return nil 703 } 704 705 // splitDirective splits a comma-separated directive value into its component 706 // parts, trimming each of any whitespace characters. 707 func splitValue(value string) []string { 708 parts := strings.Split(value, ",") 709 values := make([]string, 0, len(parts)) 710 for _, part := range parts { 711 values = append(values, strings.TrimSpace(part)) 712 } 713 return values 714 } 715 716 // findRulesGoVersion attempts to infer the version of io_bazel_rules_go. 717 // It can read the external directory (if bazel has fetched it), or it can 718 // read WORKSPACE. Neither method is completely reliable. 719 func findRulesGoVersion(c *config.Config) (version.Version, error) { 720 const message = `Gazelle may not be compatible with this version of rules_go. 721 Update io_bazel_rules_go to a newer version in your WORKSPACE file.` 722 723 var vstr string 724 if rulesGoPath, err := repo.FindExternalRepo(c.RepoRoot, config.RulesGoRepoName); err == nil { 725 // Bazel has already fetched io_bazel_rules_go. We can read its version 726 // from //go:def.bzl. 727 defBzlPath := filepath.Join(rulesGoPath, "go", "def.bzl") 728 defBzlContent, err := os.ReadFile(defBzlPath) 729 if err != nil { 730 return nil, err 731 } 732 versionRe := regexp.MustCompile(`(?m)^RULES_GO_VERSION = ['"]([0-9.]*)['"]`) 733 match := versionRe.FindSubmatch(defBzlContent) 734 if match == nil { 735 return nil, fmt.Errorf("RULES_GO_VERSION not found in @%s//go:def.bzl.\n%s", config.RulesGoRepoName, message) 736 } 737 vstr = string(match[1]) 738 } else { 739 // Bazel has not fetched io_bazel_rules_go. Maybe we can find it in the 740 // WORKSPACE file. 741 re := regexp.MustCompile(`github\.com/bazelbuild/rules_go/releases/download/v([0-9.]+)/`) 742 RepoLoop: 743 for _, r := range c.Repos { 744 if r.Kind() == "http_archive" && r.Name() == "io_bazel_rules_go" { 745 for _, u := range r.AttrStrings("urls") { 746 if m := re.FindStringSubmatch(u); m != nil { 747 vstr = m[1] 748 break RepoLoop 749 } 750 } 751 } 752 } 753 } 754 755 if vstr == "" { 756 // Couldn't find a version. We return a specific value since this is not 757 // usually a useful error to report. 758 return nil, errRulesGoRepoNotFound 759 } 760 761 return version.ParseVersion(vstr) 762 } 763 764 var errRulesGoRepoNotFound = errors.New(config.RulesGoRepoName + " external repository not found") 765 766 // detectNamingConvention attempts to detect the naming convention in use by 767 // reading build files in subdirectories of the repository root directory. 768 // 769 // If detectNamingConvention can't detect the naming convention (for example, 770 // because no build files are found or multiple naming conventions are found), 771 // importNamingConvention is returned. 772 func detectNamingConvention(c *config.Config, rootFile *rule.File) namingConvention { 773 if !c.IndexLibraries { 774 // Indexing is disabled, which usually means speed is important and I/O 775 // should be minimized. Let's not open extra files or directories. 776 return importNamingConvention 777 } 778 779 detectInFile := func(f *rule.File) namingConvention { 780 for _, r := range f.Rules { 781 // NOTE: map_kind is not supported. c.KindMap will not be accurate in 782 // subdirectories. 783 kind := r.Kind() 784 name := r.Name() 785 if kind != "alias" && name == defaultLibName { 786 // Assume any kind of rule with the name "go_default_library" is some 787 // kind of go library. The old version of go_proto_library used this 788 // name, and it's possible with map_kind as well. 789 return goDefaultLibraryNamingConvention 790 } else if isGoLibrary(kind) && name == path.Base(r.AttrString("importpath")) { 791 return importNamingConvention 792 } 793 } 794 return unknownNamingConvention 795 } 796 797 detectInDir := func(dir, rel string) namingConvention { 798 var f *rule.File 799 for _, name := range c.ValidBuildFileNames { 800 fpath := filepath.Join(dir, name) 801 data, err := os.ReadFile(fpath) 802 if err != nil { 803 continue 804 } 805 f, err = rule.LoadData(fpath, rel, data) 806 if err != nil { 807 continue 808 } 809 } 810 if f == nil { 811 return unknownNamingConvention 812 } 813 return detectInFile(f) 814 } 815 816 nc := unknownNamingConvention 817 if rootFile != nil { 818 if rootNC := detectInFile(rootFile); rootNC != unknownNamingConvention { 819 return rootNC 820 } 821 } 822 823 ents, err := os.ReadDir(c.RepoRoot) 824 if err != nil { 825 return importNamingConvention 826 } 827 for _, ent := range ents { 828 if !ent.IsDir() { 829 continue 830 } 831 dirName := ent.Name() 832 dirNC := detectInDir(filepath.Join(c.RepoRoot, dirName), dirName) 833 if dirNC == unknownNamingConvention { 834 continue 835 } 836 if nc != unknownNamingConvention && dirNC != nc { 837 // Subdirectories use different conventions. Return the default. 838 return importNamingConvention 839 } 840 nc = dirNC 841 } 842 if nc == unknownNamingConvention { 843 return importNamingConvention 844 } 845 return nc 846 }