diff --git a/BUILD.bazel b/BUILD.bazel index 44ccd5685..2e934a7a5 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -81,6 +81,7 @@ filegroup( "repository.rst", "//cmd:all_files", "//config:all_files", + "//convention:all_files", "//flag:all_files", "//internal:all_files", "//label:all_files", diff --git a/README.rst b/README.rst index ba48ffdb6..f4e43ab2d 100644 --- a/README.rst +++ b/README.rst @@ -517,6 +517,14 @@ The following flags are accepted: | should use the index to resolve dependencies. If this is switched off, Gazelle would rely on | | ``# gazelle:prefix`` directive or ``-go_prefix`` flag to resolve dependencies. | +-------------------------------------------------------------------+----------------------------------------+ +| :flag:`-use_conventions true|false` | :value:`false` | ++-------------------------------------------------------------------+----------------------------------------+ +| Usually used in combindation with `-index=false`, when enabled, this flag will check gazelle languages | +| for compliance with the `Convention` interface. Enabling the convention checker will add | +| `# gazelle:resolve` directives to the root `BUILD.bazel` that do not follow the convention. | +| `CheckConvention(c *config.Config, kind, imp, name, rel string) bool` should be written to return false | +| for the a target and import path pair that do not follow the convention. | ++-------------------------------------------------------------------+----------------------------------------+ | :flag:`-go_grpc_compiler` | ``@io_bazel_rules_go//proto:go_grpc`` | +-------------------------------------------------------------------+----------------------------------------+ | The protocol buffers compiler to use for building go bindings for gRPC. May be repeated. | diff --git a/cmd/gazelle/BUILD.bazel b/cmd/gazelle/BUILD.bazel index 3a87c0880..e90c1ad0f 100644 --- a/cmd/gazelle/BUILD.bazel +++ b/cmd/gazelle/BUILD.bazel @@ -25,6 +25,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//config", + "//convention", "//flag", "//internal/wspace", "//label", diff --git a/cmd/gazelle/fix-update.go b/cmd/gazelle/fix-update.go index 52e6af2d5..f6e7505ab 100644 --- a/cmd/gazelle/fix-update.go +++ b/cmd/gazelle/fix-update.go @@ -35,6 +35,7 @@ import ( "github.com/bazelbuild/bazel-gazelle/internal/wspace" "github.com/bazelbuild/bazel-gazelle/label" "github.com/bazelbuild/bazel-gazelle/language" + "github.com/bazelbuild/bazel-gazelle/convention" "github.com/bazelbuild/bazel-gazelle/merger" "github.com/bazelbuild/bazel-gazelle/repo" "github.com/bazelbuild/bazel-gazelle/resolve" @@ -55,6 +56,7 @@ type updateConfig struct { patchBuffer bytes.Buffer print0 bool profile profiler + useConventions bool } type emitFunc func(c *config.Config, f *rule.File) error @@ -90,6 +92,7 @@ func (ucr *updateConfigurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *conf fs.StringVar(&ucr.mode, "mode", "fix", "print: prints all of the updated BUILD files\n\tfix: rewrites all of the BUILD files in place\n\tdiff: computes the rewrite but then just does a diff") fs.BoolVar(&ucr.recursive, "r", true, "when true, gazelle will update subdirectories recursively") + fs.BoolVar(&uc.useConventions, "use_conventions", false, "when true, gazelle will enable convention checking in fix-update.") fs.StringVar(&uc.patchPath, "patch", "", "when set with -mode=diff, gazelle will write to a file instead of stdout") fs.BoolVar(&uc.print0, "print0", false, "when set with -mode=fix, gazelle will print the names of rewritten files separated with \\0 (NULL)") fs.StringVar(&ucr.cpuProfile, "cpuprofile", "", "write cpu profile to `file`") @@ -263,7 +266,8 @@ func runFixUpdate(wd string, cmd command, args []string) (err error) { &config.CommonConfigurer{}, &updateConfigurer{}, &walk.Configurer{}, - &resolve.Configurer{}) + &resolve.Configurer{}, + &convention.Configurer{}) for _, lang := range languages { cexts = append(cexts, lang) @@ -313,6 +317,7 @@ func runFixUpdate(wd string, cmd command, args []string) (err error) { } }() + conventionChecker := convention.NewChecker(c, uc.dirs, mrslv.Resolver, exts...) var errorsFromWalk []error walk.Walk(c, cexts, uc.dirs, uc.walkMode, func(dir, rel string, c *config.Config, update bool, f *rule.File, subdirs, regularFiles, genFiles []string) { // If this file is ignored or if Gazelle was not asked to update this @@ -321,9 +326,14 @@ func runFixUpdate(wd string, cmd command, args []string) (err error) { for _, repl := range c.KindMap { mrslv.MappedKind(rel, repl) } - if c.IndexLibraries && f != nil { + if f != nil { for _, r := range f.Rules { - ruleIndex.AddRule(c, r, f) + if c.IndexLibraries { + ruleIndex.AddRule(c, r, f) + } + if uc.useConventions { + conventionChecker.AddRule(c, r, f) + } } } return @@ -448,10 +458,13 @@ func runFixUpdate(wd string, cmd command, args []string) (err error) { }) // Add library rules to the dependency resolution table. - if c.IndexLibraries { - for _, r := range f.Rules { + for _, r := range f.Rules { + if c.IndexLibraries { ruleIndex.AddRule(c, r, f) } + if uc.useConventions { + conventionChecker.AddRule(c, r, f) + } } }) @@ -475,6 +488,9 @@ func runFixUpdate(wd string, cmd command, args []string) (err error) { } // Finish building the index for dependency resolution. + if uc.useConventions { + conventionChecker.Finish(c, ruleIndex) + } ruleIndex.Finish() // Resolve dependencies. diff --git a/convention/BUILD.bazel b/convention/BUILD.bazel new file mode 100644 index 000000000..cfe55ccf5 --- /dev/null +++ b/convention/BUILD.bazel @@ -0,0 +1,55 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "convention", + srcs = [ + "check.go", + "config.go", + "dir_set.go", + ], + importpath = "github.com/bazelbuild/bazel-gazelle/convention", + visibility = ["//visibility:public"], + deps = [ + "//config", + "//label", + "//resolve", + "//rule", + ], +) + +alias( + name = "go_default_library", + actual = ":convention", + visibility = ["//visibility:public"], +) + +go_test( + name = "convention_test", + srcs = [ + "check_test.go", + "config_test.go", + ], + embed = [":convention"], + deps = [ + "//config", + "//label", + "//language/go", + "//language/proto", + "//resolve", + "//rule", + ], +) + +filegroup( + name = "all_files", + testonly = True, + srcs = [ + "BUILD.bazel", + "check.go", + "check_test.go", + "config.go", + "config_test.go", + "dir_set.go", + ], + visibility = ["//visibility:public"], +) diff --git a/convention/check.go b/convention/check.go new file mode 100644 index 000000000..ff8a77592 --- /dev/null +++ b/convention/check.go @@ -0,0 +1,309 @@ +// Copyright 2024 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 convention + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/label" + "github.com/bazelbuild/bazel-gazelle/resolve" + "github.com/bazelbuild/bazel-gazelle/rule" +) + +const autoResolvesHeader = "### AUTOMATIC RESOLVES ###" + +// Convention should be implemented by langauge extensions in order to +// register language specific convention logic with the convention checker. +type Convention interface { + // CheckConvention returns whether or not the rule information follows + // a known convention. + CheckConvention(c *config.Config, kind, imp, name, rel string) bool +} + +// resolveSpec contains the rule information used by Finish() to create +// new resolve directives. +// rule and file are the rule's underlying syntax information. +type resolveSpec struct { + imps []resolve.ImportSpec + label label.Label + rule *rule.Rule + file *rule.File +} + +// metaResolver returns the Resolver associated with the given +// rule and package, or nil if it doesn't have one. +type metaResolver func(r *rule.Rule, pkgRel string) resolve.Resolver + +// Checker checks to see if rules follow a convention, and if not writes +// them as # resolve directive to the top level BUILD.bazel. +type Checker struct { + dirsRel []string + metaResolver metaResolver + resolves []resolveSpec + conventions []Convention +} + +// NewChecker creates a new Checker object. +// dirs is the list of passed in dirs for this Gazelle run. +// metaresolver is used for getting a rule's imports. +// exts is used for finding the conventions for a given rule. +func NewChecker(c *config.Config, dirs []string, metaResolver func(r *rule.Rule, pkgRel string) resolve.Resolver, exts ...interface{}) *Checker { + if !isEnabled(c) { + return &Checker{} + } + var conventions []Convention + for _, e := range exts { + if c, ok := e.(Convention); ok { + conventions = append(conventions, c) + } + } + checker := &Checker{ + metaResolver: metaResolver, + conventions: conventions, + } + checker.dirsRel = make([]string, len(dirs)) + for i, d := range dirs { + if d == c.RepoRoot { + checker.dirsRel[i] = "" + } else { + checker.dirsRel[i] = strings.TrimPrefix(d, c.RepoRoot+string(filepath.Separator)) + } + } + return checker +} + +func isEnabled(c *config.Config) bool { + cc := getConventionConfig(c) + return cc != nil && cc.genResolves +} + +// AddRule checks all of supplied rule's imports against the rule's corresponding convention. +// Rules that don't match convention are saved, so they can be written as top-level # resolve +// directives once Finish() is called. +func (ch *Checker) AddRule(c *config.Config, r *rule.Rule, f *rule.File) { + if !isEnabled(c) { + return + } + var imps []resolve.ImportSpec + if rslv := ch.metaResolver(r, f.Pkg); rslv != nil { + imps = rslv.Imports(c, r, f) + } + unconventionalImps := imps[:0] + for _, imp := range imps { + var conventional bool + for _, conv := range ch.conventions { + if conv.CheckConvention(c, r.Kind(), imp.Imp, r.Name(), f.Pkg) { + conventional = true + break + } + } + if !conventional { + unconventionalImps = append(unconventionalImps, imp) + } + } + if len(unconventionalImps) > 0 { + ch.resolves = append(ch.resolves, resolveSpec{ + imps: unconventionalImps, + label: label.New("", f.Pkg, r.Name()), + rule: r, + file: f, + }) + } +} + +type directive struct { + imp resolve.ImportSpec + label label.Label +} + +// expected format: # gazelle:resolve source-language import-language import-string label +// for example: # gazelle:resolve go go example.com/foo //src/foo:go_default_library +func parseDirective(line string) (d directive, err error) { + parts := strings.Fields(strings.TrimPrefix(line, "# gazelle:resolve ")) + var imp resolve.ImportSpec + switch len(parts) { + case 3: + imp.Lang = parts[0] + imp.Imp = parts[1] + case 4: + imp.Lang = parts[1] + imp.Imp = parts[2] + default: + return d, fmt.Errorf("could not parse directive: %q\n\texpected # gazelle:resolve source-language [import-language] import-string label", line) + } + label, err := label.Parse(parts[len(parts)-1]) + if err != nil { + return d, fmt.Errorf("invalid label %q: %v", line, err) + } + return directive{ + label: label, + imp: resolve.ImportSpec{ + Lang: parts[1], + Imp: parts[2], + }, + }, err +} + +// getHeaderPosition scans the file and returns the position of the autoResolvesHeader +func getHeaderPosition(r io.Reader) (int64, error) { + scanner := bufio.NewScanner(r) + var startPos int64 + for scanner.Scan() { + line := scanner.Text() + startPos += int64(len(line)) + int64(1) // +1 for new line + if line == autoResolvesHeader { + return startPos, scanner.Err() + } + } + return startPos, fmt.Errorf("Failure finding header: %s in top-level BUILD.bazel", autoResolvesHeader) +} + +// readDirectives seeks the file to startPos and then reads in all of the +// # gazelle:resolve directives into an import => directive map +func readDirectives(r *os.File, startPos int64) (map[string]directive, error) { + if _, err := r.Seek(startPos, 0); err != nil { + return nil, err + } + directivesMap := make(map[string]directive) // import => directive + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + d, err := parseDirective(line) + if err != nil { + return directivesMap, err + } + directivesMap[d.imp.Imp] = d + } + return directivesMap, scanner.Err() +} + +func (ch *Checker) finish(c *config.Config, index *resolve.RuleIndex) error { + if !isEnabled(c) { + return nil + } + f, err := os.OpenFile(path.Join(c.RepoRoot, "BUILD.bazel"), os.O_RDWR, 0644) + if err != nil { + return err + } + defer f.Close() + + headerPos, err := getHeaderPosition(f) + if err != nil { + return err + } + directivesMap, err := readDirectives(f, headerPos) // import => directive + if err != nil { + return err + } + + hasNewResolve := false + // Collect resolve directives from newly generated rules during current Gazelle run. + // Because Gazelle only updates rules in ch.dirsRel, these new resolves all point to those directories. + newDirectives := make(map[string]directive) + for _, spec := range ch.resolves { + hasNew := false + for _, imp := range spec.imps { + newDirectives[imp.Imp] = directive{ + label: spec.label, + imp: imp, + } + if d, ok := directivesMap[imp.Imp]; !ok || !d.label.Equal(spec.label) || d.imp.Lang != imp.Lang { + hasNew = true + } + } + if hasNew { + hasNewResolve = true + // enable indexing and add the unconventional rule to the index + // that way they can be resolved by the current Gazelle run + c.IndexLibraries = true + index.AddRule(c, spec.rule, spec.file) + } + } + + cc := getConventionConfig(c) + givenDirs := NewDirSet(ch.dirsRel) + directives, hasOutdatedResolve := replaceDirectivesInScope(directivesMap, newDirectives, func(dir string) bool { + return cc.recursiveMode && givenDirs.HasSubDir(dir) || !cc.recursiveMode && givenDirs.hasDir(dir) + }) + if !hasOutdatedResolve && !hasNewResolve { + // no new directives and no old directives, don't need to rewrite + return nil + } + + // write updated directive list back to BUILD.bazel beginning at headerPos + if err := f.Truncate(headerPos); err != nil { + return err + } + if _, err := f.Seek(headerPos, 0); err != nil { + return err + } + w := bufio.NewWriter(f) + for _, d := range directives { + if _, err := fmt.Fprintf(w, "# gazelle:resolve %s %s %s %s\n", d.imp.Lang, d.imp.Lang, d.imp.Imp, d.label); err != nil { + return err + } + } + return w.Flush() +} + +// Finish reads all of the specs collected by AddRule, and for any that do not already have +// a # resolve directive, they are added to the sorted list within the top-level BUILD.bazel. +// New specs are also added to the RuleIndex, that way they can be resolved by the +// current Gazelle run. +func (ch *Checker) Finish(c *config.Config, index *resolve.RuleIndex) { + if err := ch.finish(c, index); err != nil { + log.Println(err) + } +} + +func replaceDirectivesInScope(existingDirectives, newDirectives map[string]directive, isInScope func(string) bool) ([]directive, bool) { + reducedMap := make(map[string]directive, len(existingDirectives)) + for imp, d := range existingDirectives { + if isInScope(d.label.Pkg) { + if newDirective, ok := newDirectives[imp]; !ok || !d.label.Equal(newDirective.label) || d.imp.Lang != newDirective.imp.Lang { + // either the directive is no longer needed, or it resolves to a different language or label. + continue + } + } + reducedMap[imp] = d + } + hasOutdatedResolve := len(reducedMap) < len(existingDirectives) + // adding new directives, and possibly updating outdated directives that map an import to a different language or label. + for imp, d := range newDirectives { + reducedMap[imp] = d + } + + // sort directives, first by lang and then by import, making it ready to be written back to disk. + directives := make([]directive, 0, len(reducedMap)) + for _, d := range reducedMap { + directives = append(directives, d) + } + sort.Slice(directives, func(i, j int) bool { + if directives[i].imp.Lang != directives[j].imp.Lang { + return directives[i].imp.Lang < directives[j].imp.Lang + } + return directives[i].imp.Imp < directives[j].imp.Imp + }) + return directives, hasOutdatedResolve +} diff --git a/convention/check_test.go b/convention/check_test.go new file mode 100644 index 000000000..7625c30dc --- /dev/null +++ b/convention/check_test.go @@ -0,0 +1,331 @@ +// 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 convention + +import ( + "os" + "path" + "reflect" + "testing" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/label" + golang "github.com/bazelbuild/bazel-gazelle/language/go" + "github.com/bazelbuild/bazel-gazelle/language/proto" + "github.com/bazelbuild/bazel-gazelle/resolve" + "github.com/bazelbuild/bazel-gazelle/rule" +) + +type testConvention struct { +} + +func (*testConvention) CheckConvention(c *config.Config, kind, imp, name, rel string) bool { + return kind == "go_library" +} + +func TestAddRule(t *testing.T) { + goResolve := golang.NewLanguage() + protoResolve := proto.NewLanguage() + msrlv := func(r *rule.Rule, pkgRel string) resolve.Resolver { + switch r.Kind() { + case "proto_library": + return protoResolve + case "go_library": + return goResolve + default: + return nil + } + } + + c := config.New() + f := &rule.File{Pkg: ""} + c.Exts[_conventionName] = &conventionConfig{genResolves: true} + chk := NewChecker(c, nil, msrlv, &testConvention{}) + + var wantResolves []resolveSpec + + // conventional go_library rule + r := rule.NewRule("go_library", "") + r.SetAttr("importpath", "code.internal/foo") + chk.AddRule(c, r, f) + + // unconventional proto_library rule + r = rule.NewRule("proto_library", "") + r.SetAttr("srcs", rule.ExprFromValue([]string{"foo/bar.proto"})) + wantResolves = append(wantResolves, resolveSpec{ + imps: []resolve.ImportSpec{ + resolve.ImportSpec{Lang: "proto", Imp: "foo/bar.proto"}, + }, + label: label.New("", f.Pkg, r.Name()), + file: f, + rule: r, + }) + chk.AddRule(c, r, f) + + // rule with no Resolver is ignored + r = rule.NewRule("foo_library", "") + chk.AddRule(c, r, f) + + if !reflect.DeepEqual(wantResolves, chk.resolves) { + t.Errorf("got resolves %v; want %v", chk.resolves, wantResolves) + } +} + +func TestParseDirective(t *testing.T) { + tests := []struct { + name string + inLine string + wantDirective directive + wantError bool + }{ + { + name: "proper format", + inLine: "# gazelle:resolve go go example.com/foo //src/foo:go_default_library", + wantDirective: directive{ + imp: resolve.ImportSpec{Imp: "example.com/foo", Lang: "go"}, + label: label.New("", "src/foo", "go_default_library"), + }, + }, + { + name: "missing lang", + inLine: "# gazelle:resolve example.com/foo //src/foo:go_default_library", + wantError: true, + }, + { + name: "extra space", + inLine: " # gazelle:resolve go example.com/foo //src/foo:go_default_library", + wantError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotDirective, err := parseDirective(tt.inLine) + if tt.wantError { + if err == nil { + t.Error("err should not be nil") + } + return + } + if err != nil { + t.Errorf("err %v should not be nil", err) + } + if tt.wantDirective != gotDirective { + t.Errorf("got directive %v; want %v", gotDirective, tt.wantDirective) + } + }) + } +} + +func TestFinish(t *testing.T) { + mrslv := func(r *rule.Rule, pkgRel string) resolve.Resolver { + return nil + } + + startContent := `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve go go code.internal/baz //src/code.internal/bar:go_default_library +# gazelle:resolve proto proto idl/foo/bar.proto //idl/foo:foopb_proto +` + tests := []struct { + name string + inContent string + inResolveSpecs []resolveSpec + wantContent string + dirs []string + recursive bool + }{ + { + name: "no new resolves", + inContent: startContent, + wantContent: startContent, + dirs: []string{"src/code.internal/devexp"}, + }, + { + name: "new resolves", + inContent: startContent, + inResolveSpecs: []resolveSpec{ + // unconventional go_library rule + resolveSpec{ + imps: []resolve.ImportSpec{resolve.ImportSpec{Imp: "code.internal/foo/baz", Lang: "go"}}, + label: label.New("", "src/code.internal/foo/bar", "go_default_library"), + rule: &rule.Rule{}, + file: &rule.File{}, + }, + // unconventional proto rule + resolveSpec{ + imps: []resolve.ImportSpec{ + resolve.ImportSpec{Imp: "idl/foo/bar.proto", Lang: "proto"}, + resolve.ImportSpec{Imp: "idl/foo/baz.proto", Lang: "proto"}, + }, + label: label.New("", "idl/foo", "foopb_proto"), + rule: &rule.Rule{}, + file: &rule.File{}, + }, + }, + wantContent: `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve go go code.internal/baz //src/code.internal/bar:go_default_library +# gazelle:resolve go go code.internal/foo/baz //src/code.internal/foo/bar:go_default_library +# gazelle:resolve proto proto idl/foo/bar.proto //idl/foo:foopb_proto +# gazelle:resolve proto proto idl/foo/baz.proto //idl/foo:foopb_proto +`, + }, + { + name: "prune on full repo run", + inContent: startContent, + inResolveSpecs: []resolveSpec{ + // unconventional go_library rule + resolveSpec{ + imps: []resolve.ImportSpec{resolve.ImportSpec{Imp: "code.internal/foo/baz", Lang: "go"}}, + label: label.New("", "src/code.internal/foo/bar", "go_default_library"), + rule: &rule.Rule{}, + file: &rule.File{}, + }, + // unconventional proto rule + resolveSpec{ + imps: []resolve.ImportSpec{ + resolve.ImportSpec{Imp: "idl/foo/bar.proto", Lang: "proto"}, + resolve.ImportSpec{Imp: "idl/foo/baz.proto", Lang: "proto"}, + }, + label: label.New("", "idl/foo", "foopb_proto"), + rule: &rule.Rule{}, + file: &rule.File{}, + }, + }, + dirs: []string{""}, + recursive: true, + wantContent: `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve go go code.internal/foo/baz //src/code.internal/foo/bar:go_default_library +# gazelle:resolve proto proto idl/foo/bar.proto //idl/foo:foopb_proto +# gazelle:resolve proto proto idl/foo/baz.proto //idl/foo:foopb_proto +`, + }, + { + name: "prune for subtree", + inContent: startContent, + dirs: []string{"src"}, + recursive: true, + wantContent: `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve proto proto idl/foo/bar.proto //idl/foo:foopb_proto +`, + }, + { + name: "prune for one directory but not its subdirectories", + inContent: `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve go go code.internal/foo/baz //src/code.internal/foo/bar:go_default_library +# gazelle:resolve go go code.internal/foo //src/code.internal/foo:go_mock_library +`, + dirs: []string{"src/code.internal/foo"}, + wantContent: `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve go go code.internal/foo/baz //src/code.internal/foo/bar:go_default_library +`, + }, + { + name: "should not prune outside given dirs", + inContent: startContent, + dirs: []string{"src/code.internal/bar"}, + wantContent: `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve proto proto idl/foo/bar.proto //idl/foo:foopb_proto +`, + }, + { + name: "update import path", + inContent: startContent, + inResolveSpecs: []resolveSpec{ + { + imps: []resolve.ImportSpec{ + {Imp: "code.internal/foo/baz", Lang: "go"}, + }, + label: label.New("", "src/code.internal/bar", "go_default_library"), + rule: &rule.Rule{}, + file: &rule.File{}, + }, + }, + dirs: []string{"src/code.internal/bar"}, + wantContent: `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve go go code.internal/foo/baz //src/code.internal/bar:go_default_library +# gazelle:resolve proto proto idl/foo/bar.proto //idl/foo:foopb_proto +`, + }, + { + name: "update label", + inContent: startContent, + inResolveSpecs: []resolveSpec{ + { + imps: []resolve.ImportSpec{ + {Imp: "code.internal/baz", Lang: "go"}, + }, + label: label.New("", "src/code.internal/bar/foo", "go_default_library"), + rule: &rule.Rule{}, + file: &rule.File{}, + }, + }, + dirs: []string{"src/code.internal/bar"}, + wantContent: `# do not edit +### AUTOMATIC RESOLVES ### +# gazelle:resolve go go code.internal/baz //src/code.internal/bar/foo:go_default_library +# gazelle:resolve proto proto idl/foo/bar.proto //idl/foo:foopb_proto +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := config.New() + c.Exts[_conventionName] = &conventionConfig{ + genResolves: true, + recursiveMode: tt.recursive, + } + cext := resolve.Configurer{} + cext.RegisterFlags(nil, "", c) + + tmpDir := t.TempDir() + c.RepoRoot = tmpDir + buildFile := path.Join(tmpDir, "BUILD.bazel") + if err := os.WriteFile(buildFile, []byte(tt.inContent), 0644); err != nil { + t.Fatalf("failed to write BUILD.bazel: %v", err) + } + chk := NewChecker(c, tt.dirs, mrslv, &testConvention{}) + chk.resolves = tt.inResolveSpecs + chk.Finish(c, resolve.NewRuleIndex(mrslv)) + gotContent, err := os.ReadFile(buildFile) + if err != nil { + t.Errorf("failed to read file: %v", err) + } + if tt.wantContent != string(gotContent) { + t.Errorf("got content %s; want %s", gotContent, tt.wantContent) + } + }) + } +} + +func TestReplaceDirectivesInScope(t *testing.T) { + directiveMap := map[string]directive{ + "uber.com/foo": { + imp: resolve.ImportSpec{Lang: "go"}, + label: label.Label{Pkg: "uber.com/foo", Name: "go_default_library"}, + }, + } + + if _, hasOuted := replaceDirectivesInScope(directiveMap, directiveMap, func(s string) bool { + return true + }); hasOuted { + t.Errorf("hasOuted should be false") + } +} diff --git a/convention/config.go b/convention/config.go new file mode 100644 index 000000000..2ad7d1888 --- /dev/null +++ b/convention/config.go @@ -0,0 +1,90 @@ +// Copyright 2024 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 convention + +import ( + "errors" + "flag" + "fmt" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/rule" +) + +type conventionConfig struct { + // genResolves controls whether or not gazelle will add resolve directives + // for non-conventional imports to the top level BUILD.bazel. + genResolves bool + // recursiveMode denotes if Gazelle was set to run recursively via the "-r" flag + recursiveMode bool +} + +const _conventionName = "_convention" + +// Configurer is convention's implementation of the config.Configurer interface. +type Configurer struct{} + +func getConventionConfig(c *config.Config) *conventionConfig { + cc := c.Exts[_conventionName] + if cc == nil { + return &conventionConfig{} + } + return cc.(*conventionConfig) +} + +// RegisterFlags registers the genResolves flag, used to enable/disable this library. +func (*Configurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) { + cc := getConventionConfig(c) + switch cmd { + case "fix", "update": + fs.BoolVar(&cc.genResolves, "resolveGen", false, "whether gazelle will add resolve directives for non-conventional imports in the top level BUILD.bazel") + } + c.Exts[_conventionName] = cc +} + +// getRecursiveMode looks up the "r" flag's value, which should be registered by +// the upstream cmd/gazelle/fix-update.go Configurer. +func getRecursiveMode(fs *flag.FlagSet) (bool, error) { + f := fs.Lookup("r") + if f == nil { + return false, errors.New("expected -r flag to be set") + } + g, ok := f.Value.(flag.Getter) + if !ok { + return false, fmt.Errorf("got data of type %T but wanted flag.Getter", g) + } + recursiveMode, ok := g.Get().(bool) + if !ok { + return false, fmt.Errorf("got data of type %T but wanted bool", recursiveMode) + } + return recursiveMode, nil +} + +// CheckFlags determines the value of conventionConfig.recursiveMode +func (*Configurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error { + cc := getConventionConfig(c) + var err error + cc.recursiveMode, err = getRecursiveMode(fs) + return err +} + +// KnownDirectives implements config.Configurer interface. +func (*Configurer) KnownDirectives() []string { return nil } + +// Configure implements config.Configurer interface. +func (*Configurer) Configure(c *config.Config, rel string, f *rule.File) { + cc := *getConventionConfig(c) + c.Exts[_conventionName] = &cc +} diff --git a/convention/config_test.go b/convention/config_test.go new file mode 100644 index 000000000..2c4b18b0d --- /dev/null +++ b/convention/config_test.go @@ -0,0 +1,45 @@ +// Copyright 2024 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 convention + +import ( + "flag" + "testing" + + "github.com/bazelbuild/bazel-gazelle/config" +) + +func TestConfig(t *testing.T) { + c := config.New() + cc := &conventionConfig{} + c.Exts[_conventionName] = cc + fs := &flag.FlagSet{} + fs.Bool("r", false, "") + cext := Configurer{} + cext.RegisterFlags(fs, "update", c) + err := fs.Parse([]string{"-r=true", "-resolveGen=true"}) + if err != nil { + t.Errorf("err should be nil: %v", err) + } + if err := cext.CheckFlags(fs, c); err != nil { + t.Errorf("cext.CheckFlags err should be nil: %v", err) + } + if !cc.genResolves { + t.Error("cc.genResolves should be true") + } + if !cc.recursiveMode { + t.Error("cc.recursiveMode should be true") + } +} diff --git a/convention/dir_set.go b/convention/dir_set.go new file mode 100644 index 000000000..3555783b5 --- /dev/null +++ b/convention/dir_set.go @@ -0,0 +1,56 @@ +// Copyright 2024 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 convention + +import ( + "path/filepath" + "strings" +) + +// DirSet keeps track of a set of relative paths +type DirSet struct { + dirs map[string]bool + hasRoot bool +} + +// NewDirSet initialize a new DirSet +func NewDirSet(dirs []string) DirSet { + ds := DirSet{dirs: make(map[string]bool)} + for _, d := range dirs { + ds.dirs[strings.TrimSpace(d)] = true + if d == "" || d == "." { + ds.hasRoot = true + } + } + return ds +} + +// HasSubDir decides whether a relative path is one of the directories or in one of their subdirectories +func (ds DirSet) HasSubDir(dir string) bool { + if ds.hasRoot { + return true + } + for ; dir != "." && dir != string(filepath.Separator); dir = filepath.Dir(dir) { + if ds.hasDir(dir) { + return true + } + } + return false +} + +func (ds DirSet) hasDir(dir string) bool { + _, ok := ds.dirs[dir] + return ok +} diff --git a/internal/go_repository_tools_srcs.bzl b/internal/go_repository_tools_srcs.bzl index 5f0c7f932..bcc3387c3 100644 --- a/internal/go_repository_tools_srcs.bzl +++ b/internal/go_repository_tools_srcs.bzl @@ -33,6 +33,10 @@ GO_REPOSITORY_TOOLS_SRCS = [ Label("//config:BUILD.bazel"), Label("//config:config.go"), Label("//config:constants.go"), + Label("//convention:BUILD.bazel"), + Label("//convention:check.go"), + Label("//convention:config.go"), + Label("//convention:dir_set.go"), Label("//flag:BUILD.bazel"), Label("//flag:flag.go"), Label("//internal:BUILD.bazel"),