1
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
45 type goConfig struct {
46
47
48 rulesGoRepoName string
49
50
51
52 rulesGoVersion version.Version
53
54
55
56 genericTags map[string]bool
57
58
59
60 prefix string
61
62
63
64 prefixRel string
65
66
67
68 prefixSet bool
69
70
71
72 importMapPrefix string
73
74
75
76 importMapPrefixRel string
77
78
79
80 depMode dependencyMode
81
82
83 goGenerateProto bool
84
85
86 goNamingConvention namingConvention
87
88
89
90 goNamingConventionExternal namingConvention
91
92
93 goProtoCompilers []string
94
95
96 goProtoCompilersSet bool
97
98
99 goGrpcCompilers []string
100
101
102 goGrpcCompilersSet bool
103
104
105
106 goRepositoryMode bool
107
108
109
110
111 goVisibility []string
112
113
114
115
116
117 moduleMode bool
118
119
120
121 repoNamingConvention map[string]namingConvention
122
123
124
125
126 submodules []moduleRepo
127
128
129 testMode testMode
130
131
132
133
134
135 buildDirectivesAttr, buildExternalAttr, buildExtraArgsAttr, buildFileGenerationAttr, buildFileNamesAttr, buildFileProtoModeAttr, buildTagsAttr string
136 }
137
138
139 type testMode int
140
141 const (
142
143 defaultTestMode = iota
144
145
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
203
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
212
213
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
238
239 type dependencyMode int
240
241 const (
242
243
244
245 externalMode dependencyMode = iota
246
247
248
249 staticMode
250
251
252
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
324 type namingConvention int
325
326 const (
327
328 unknownNamingConvention namingConvention = iota
329
330
331 goDefaultLibraryNamingConvention
332
333
334
335
336
337 importNamingConvention
338
339
340
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
471
472
473
474 gc := getGoConfig(c)
475 if pc := proto.GetProtoConfig(c); pc != nil {
476 pc.GoPrefix = gc.prefix
477 }
478
479
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
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
524
525
526
527
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
539
540
541
542
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
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
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
674 goModPath := filepath.Join(c.RepoRoot, filepath.FromSlash(rel), "go.mod")
675 goMod, err := os.ReadFile(goModPath)
676
677
678 if err == nil {
679 goModFile, err := modfile.ParseLax(goModPath, goMod, nil)
680
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
696
697
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
706
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
717
718
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
726
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
740
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
757
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
767
768
769
770
771
772 func detectNamingConvention(c *config.Config, rootFile *rule.File) namingConvention {
773 if !c.IndexLibraries {
774
775
776 return importNamingConvention
777 }
778
779 detectInFile := func(f *rule.File) namingConvention {
780 for _, r := range f.Rules {
781
782
783 kind := r.Kind()
784 name := r.Name()
785 if kind != "alias" && name == defaultLibName {
786
787
788
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
838 return importNamingConvention
839 }
840 nc = dirNC
841 }
842 if nc == unknownNamingConvention {
843 return importNamingConvention
844 }
845 return nc
846 }
847
View as plain text