diff --git a/README.md b/README.md index b6b2b52..7cd0b10 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ - [`scala_proto_package` conflict resolver](#scala_proto_package-conflict-resolver) - [`predefined_label` conflict resolver](#predefined_label-conflict-resolver) - [Custom conflict resolvers](#custom-conflict-resolvers) + - [Dependency List Cleanup](#dependency-list-cleanup) - [Cache](#cache) - [Profiling](#profiling) - [CPU](#cpu) @@ -715,6 +716,20 @@ type customConflictResolver struct {} ... ``` +### Dependency List Cleanup + +In some cases it is necessary to visit the resolved dependency list for a rule +and apply custom logic to cleanup labels. The `resolver.DepsCleaner` interface +should be implemented and registered to a global cache, similar to how it is +done with "Custom confict resolvers". + +To use a deps cleaner, you'll need to do the following: + +- implement the `resolver.DepsCleaner` interface (see code for details). +- register your implementation in an init function via `resolver.GlobalDepsCleanerRegistry().PutDepsCleaner(name, impl)` using a unique name (e.g. a hypothetical `proto_deps_cleaner`). +- Make the deps cleaner available in your gazelle rule via arguments `--scala_deps_cleaner=proto_deps_cleaner`. +- Enable the deps cleaner in a BUILD file via the directive `# gazelle:scala_deps_cleaner proto_deps_cleaner`. + ## Cache Parsing scala source files for a large repository is expensive. A cache can be diff --git a/language/scala/BUILD.bazel b/language/scala/BUILD.bazel index 8b6a4c5..2f14ef5 100644 --- a/language/scala/BUILD.bazel +++ b/language/scala/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "configure.go", "conflict_resolver_registry.go", "cross_resolve.go", + "deps_cleaner_registry.go", "existing_scala_rule.go", "fix.go", "flags.go", diff --git a/language/scala/deps_cleaner_registry.go b/language/scala/deps_cleaner_registry.go new file mode 100644 index 0000000..87b751b --- /dev/null +++ b/language/scala/deps_cleaner_registry.go @@ -0,0 +1,24 @@ +package scala + +import ( + "fmt" + + "github.com/stackb/scala-gazelle/pkg/resolver" +) + +// GetDepsCleaner implements part of the resolver.DepsCleanerRegistry +// interface. +func (sl *scalaLang) GetDepsCleaner(name string) (resolver.DepsCleaner, bool) { + r, ok := sl.depsCleaners[name] + return r, ok +} + +// PutDepsCleaner implements part of the resolver.DepsCleanerRegistry +// interface. +func (sl *scalaLang) PutDepsCleaner(name string, r resolver.DepsCleaner) error { + if _, ok := sl.depsCleaners[name]; ok { + return fmt.Errorf("duplicate conflict resolver: %s", name) + } + sl.depsCleaners[name] = r + return nil +} diff --git a/language/scala/existing_scala_rule.go b/language/scala/existing_scala_rule.go index 2ca6484..352b6c7 100644 --- a/language/scala/existing_scala_rule.go +++ b/language/scala/existing_scala_rule.go @@ -134,6 +134,7 @@ func (s *existingScalaRule) Resolve(rctx *scalarule.ResolveContext, importsRaw i newImports := imports.Deps(sc.MaybeRewrite(r.Kind(), rctx.From)) depLabels := sc.CleanDeps(rctx.From, r.Attr("deps"), newImports) + scalaconfig.MergeDeps(r.Kind(), depLabels, newImports) if len(depLabels.List) > 0 { r.SetAttr("deps", depLabels) diff --git a/language/scala/flags.go b/language/scala/flags.go index b8d252a..8a369ce 100644 --- a/language/scala/flags.go +++ b/language/scala/flags.go @@ -19,6 +19,7 @@ import ( const ( scalaSymbolProviderFlagName = "scala_symbol_provider" scalaConflictResolverFlagName = "scala_conflict_resolver" + scalaDepsCleanerFlagName = "scala_deps_cleaner" existingScalaBinaryRuleFlagName = "existing_scala_binary_rule" existingScalaLibraryRuleFlagName = "existing_scala_library_rule" existingScalaTestRuleFlagName = "existing_scala_test_rule" @@ -40,6 +41,7 @@ func (sl *scalaLang) RegisterFlags(flags *flag.FlagSet, cmd string, c *config.Co flags.StringVar(&sl.memprofileFlagValue, memprofileFileFlagName, "", "optional path a memory profile file (.prof)") flags.Var(&sl.symbolProviderNamesFlagValue, scalaSymbolProviderFlagName, "name of a symbol provider implementation to enable") flags.Var(&sl.conflictResolverNamesFlagValue, scalaConflictResolverFlagName, "name of a conflict resolver implementation to enable") + flags.Var(&sl.depsCleanerNamesFlagValue, scalaDepsCleanerFlagName, "name of a deps cleaner implementation to enable") flags.Var(&sl.existingScalaBinaryRulesFlagValue, existingScalaBinaryRuleFlagName, "LOAD%NAME mapping for a custom existing scala binary rule implementation (e.g. '@io_bazel_rules_scala//scala:scala.bzl%scalabinary'") flags.Var(&sl.existingScalaLibraryRulesFlagValue, existingScalaLibraryRuleFlagName, "LOAD%NAME mapping for a custom existing scala library rule implementation (e.g. '@io_bazel_rules_scala//scala:scala.bzl%scala_library'") flags.Var(&sl.existingScalaTestRulesFlagValue, existingScalaTestRuleFlagName, "LOAD%NAME mapping for a custom existing scala test rule implementation (e.g. '@io_bazel_rules_scala//scala:scala.bzl%scala_test'") @@ -132,6 +134,20 @@ func (sl *scalaLang) setupConflictResolvers(flags *flag.FlagSet, c *config.Confi return nil } +func (sl *scalaLang) setupDepsCleaners(flags *flag.FlagSet, c *config.Config, names []string) error { + for _, name := range sl.depsCleanerNamesFlagValue { + cleaner, ok := resolver.GlobalDepsCleanerRegistry().GetDepsCleaner(name) + if !ok { + return fmt.Errorf("-%s not found: %q", scalaDepsCleanerFlagName, name) + } + if err := cleaner.CheckFlags(flags, c); err != nil { + return err + } + sl.depsCleaners[name] = cleaner + } + return nil +} + func (sl *scalaLang) setupExistingScalaBinaryRules(rules []string) error { for _, fqn := range rules { parts := strings.SplitN(fqn, "%", 2) diff --git a/language/scala/language.go b/language/scala/language.go index 5b5e632..7787e47 100644 --- a/language/scala/language.go +++ b/language/scala/language.go @@ -39,6 +39,9 @@ type scalaLang struct { // conflictResolverNamesFlagValue is a repeatable list of conflict resolver // to enable conflictResolverNamesFlagValue collections.StringSlice + // depsCleanerNamesFlagValue is a repeatable list of deps cleaners + // to enable + depsCleanerNamesFlagValue collections.StringSlice // existingScalaLibraryRulesFlagValue is the value of the // existing_scala_binary_rule repeatable flag existingScalaBinaryRulesFlagValue collections.StringSlice @@ -70,8 +73,10 @@ type scalaLang struct { progress mobyprogress.Output // knownRules is a map of all known generated rules knownRules map[label.Label]*rule.Rule - // conflictResolvers is a map of all known generated rules + // conflictResolvers is a map of all known conflict resolver implementations conflictResolvers map[string]resolver.ConflictResolver + // depsCleaners is a map of all known deps cleaner implementations + depsCleaners map[string]resolver.DepsCleaner // globalScope includes all known symbols in the universe (minus package // symbols) globalScope resolver.Scope diff --git a/language/scala/language_test.go b/language/scala/language_test.go index d59531b..a7cefe7 100644 --- a/language/scala/language_test.go +++ b/language/scala/language_test.go @@ -20,6 +20,7 @@ func ExampleLanguage_KnownDirectives() { // scala_rule // resolve_glob // resolve_conflicts + // scala_deps_cleaner // resolve_with // resolve_file_symbol_name // resolve_kind_rewrite_name diff --git a/pkg/parser/mocks/Parser.go b/pkg/parser/mocks/Parser.go index 15b4a0d..e85eac3 100644 --- a/pkg/parser/mocks/Parser.go +++ b/pkg/parser/mocks/Parser.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks diff --git a/pkg/resolver/BUILD.bazel b/pkg/resolver/BUILD.bazel index bffd4f6..59ef61e 100644 --- a/pkg/resolver/BUILD.bazel +++ b/pkg/resolver/BUILD.bazel @@ -8,7 +8,10 @@ go_library( "conflict_resolver.go", "conflict_resolver_registry.go", "cross_symbol_resolver.go", + "deps_cleaner.go", + "deps_cleaner_registry.go", "global_conflict_resolver_registry.go", + "global_deps_cleaner_registry.go", "global_symbol_provider_registry.go", "import.go", "import_map.go", diff --git a/pkg/resolver/deps_cleaner.go b/pkg/resolver/deps_cleaner.go new file mode 100644 index 0000000..842cdc3 --- /dev/null +++ b/pkg/resolver/deps_cleaner.go @@ -0,0 +1,26 @@ +package resolver + +import ( + "flag" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/label" + "github.com/bazelbuild/bazel-gazelle/rule" +) + +// DepsCleaner implementations are capable of applying some sort of cleanup +// strategy on the post-resolved deps of a rule. +type DepsCleaner interface { + // Name is the canonical name for the resolver + Name() string + // RegisterFlags configures the flags. RegisterFlags is called for all + // resolvers whether they are enabled or not. + RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) + // CheckFlags asserts that the flags are correct. CheckFlags is only called + // if the resolver is enabled. + CheckFlags(fs *flag.FlagSet, c *config.Config) error + // CleanDeps takes the context rule and imports, and a map of labels that + // represent the incoming deps. The cleaner implementation should remove + // unwanted deps from the map. + CleanDeps(r *rule.Rule, imports ImportMap, deps map[label.Label]bool) +} diff --git a/pkg/resolver/deps_cleaner_registry.go b/pkg/resolver/deps_cleaner_registry.go new file mode 100644 index 0000000..5698db0 --- /dev/null +++ b/pkg/resolver/deps_cleaner_registry.go @@ -0,0 +1,14 @@ +package resolver + +// DepsCleanerRegistry is an index of known conflict resolvers keyed by their name. +type DepsCleanerRegistry interface { + // GetDepsCleaner returns the named resolver. If not known `(nil, + // false)` is returned. + GetDepsCleaner(name string) (DepsCleaner, bool) + + // PutDepsCleaner adds the given known rule to the registry. It is an + // error to attempt duplicate registration of the same rule twice. + // Implementations should use the google.golang.org/grpc/status.Errorf for + // error types. + PutDepsCleaner(name string, r DepsCleaner) error +} diff --git a/pkg/resolver/global_deps_cleaner_registry.go b/pkg/resolver/global_deps_cleaner_registry.go new file mode 100644 index 0000000..e992a69 --- /dev/null +++ b/pkg/resolver/global_deps_cleaner_registry.go @@ -0,0 +1,48 @@ +package resolver + +import ( + "fmt" + "sort" +) + +var globalDepsCleaners = make(globalDepsCleanerMap) + +// GlobalDepsCleanerRegistry returns a default deps cleaner registry. +// Third-party gazelle extensions can append to this list and configure their +// own implementations. +func GlobalDepsCleanerRegistry() DepsCleanerRegistry { + return globalDepsCleaners +} + +type globalDepsCleanerMap map[string]DepsCleaner + +// GetDepsCleaner implements part of the resolver.DepsCleanerRegistry +// interface. +func (r globalDepsCleanerMap) GetDepsCleaner(name string) (DepsCleaner, bool) { + resolver, ok := r[name] + return resolver, ok +} + +// PutDepsCleaner implements part of the resolver.DepsCleanerRegistry +// interface. +func (r globalDepsCleanerMap) PutDepsCleaner(name string, resolver DepsCleaner) error { + if _, ok := r[name]; ok { + return fmt.Errorf("duplicate DepsCleaner %q", name) + } + r[name] = resolver + return nil +} + +// GlobalDepsCleaners returns a sorted list of known conflict resolvers +func GlobalDepsCleaners() []DepsCleaner { + keys := make([]string, 0, len(globalDepsCleaners)) + for k := range globalDepsCleaners { + keys = append(keys, k) + } + sort.Strings(keys) + resolvers := make([]DepsCleaner, len(keys)) + for i, k := range keys { + resolvers[i] = globalDepsCleaners[k] + } + return resolvers +} diff --git a/pkg/resolver/mocks/ConflictResolver.go b/pkg/resolver/mocks/ConflictResolver.go index 6a19e00..4732892 100644 --- a/pkg/resolver/mocks/ConflictResolver.go +++ b/pkg/resolver/mocks/ConflictResolver.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks diff --git a/pkg/resolver/mocks/Scope.go b/pkg/resolver/mocks/Scope.go index 904037b..31b0e8f 100644 --- a/pkg/resolver/mocks/Scope.go +++ b/pkg/resolver/mocks/Scope.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks diff --git a/pkg/resolver/mocks/SymbolProvider.go b/pkg/resolver/mocks/SymbolProvider.go index 09e5654..acbcb9f 100644 --- a/pkg/resolver/mocks/SymbolProvider.go +++ b/pkg/resolver/mocks/SymbolProvider.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks diff --git a/pkg/resolver/mocks/SymbolResolver.go b/pkg/resolver/mocks/SymbolResolver.go index 9df7661..112daf9 100644 --- a/pkg/resolver/mocks/SymbolResolver.go +++ b/pkg/resolver/mocks/SymbolResolver.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks diff --git a/pkg/resolver/mocks/Universe.go b/pkg/resolver/mocks/Universe.go index 18c2867..bc11dc6 100644 --- a/pkg/resolver/mocks/Universe.go +++ b/pkg/resolver/mocks/Universe.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks @@ -57,6 +57,29 @@ func (_m *Universe) GetConflictResolver(name string) (resolver.ConflictResolver, return r0, r1 } +// GetDepsCleaner provides a mock function with given fields: name +func (_m *Universe) GetDepsCleaner(name string) (resolver.DepsCleaner, bool) { + ret := _m.Called(name) + + var r0 resolver.DepsCleaner + if rf, ok := ret.Get(0).(func(string) resolver.DepsCleaner); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(resolver.DepsCleaner) + } + } + + var r1 bool + if rf, ok := ret.Get(1).(func(string) bool); ok { + r1 = rf(name) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + // GetKnownRule provides a mock function with given fields: from func (_m *Universe) GetKnownRule(from label.Label) (*rule.Rule, bool) { ret := _m.Called(from) @@ -156,6 +179,20 @@ func (_m *Universe) PutConflictResolver(name string, r resolver.ConflictResolver return r0 } +// PutDepsCleaner provides a mock function with given fields: name, r +func (_m *Universe) PutDepsCleaner(name string, r resolver.DepsCleaner) error { + ret := _m.Called(name, r) + + var r0 error + if rf, ok := ret.Get(0).(func(string, resolver.DepsCleaner) error); ok { + r0 = rf(name, r) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // PutKnownRule provides a mock function with given fields: from, r func (_m *Universe) PutKnownRule(from label.Label, r *rule.Rule) error { ret := _m.Called(from, r) diff --git a/pkg/resolver/universe.go b/pkg/resolver/universe.go index 6ec4522..af73a58 100644 --- a/pkg/resolver/universe.go +++ b/pkg/resolver/universe.go @@ -6,6 +6,7 @@ type Universe interface { SymbolProviderRegistry KnownRuleRegistry ConflictResolverRegistry + DepsCleanerRegistry Scope SymbolResolver } diff --git a/pkg/scalaconfig/config.go b/pkg/scalaconfig/config.go index 0e66bda..92e1a22 100644 --- a/pkg/scalaconfig/config.go +++ b/pkg/scalaconfig/config.go @@ -33,6 +33,7 @@ const ( scalaRuleDirective = "scala_rule" resolveGlobDirective = "resolve_glob" resolveConflictsDirective = "resolve_conflicts" + scalaDepsCleanerDirective = "scala_deps_cleaner" resolveWithDirective = "resolve_with" resolveFileSymbolName = "resolve_file_symbol_name" resolveKindRewriteNameDirective = "resolve_kind_rewrite_name" @@ -45,6 +46,7 @@ func DirectiveNames() []string { scalaRuleDirective, resolveGlobDirective, resolveConflictsDirective, + scalaDepsCleanerDirective, resolveWithDirective, resolveFileSymbolName, resolveKindRewriteNameDirective, @@ -64,6 +66,7 @@ type Config struct { labelNameRewrites map[string]resolver.LabelNameRewriteSpec annotations map[debugAnnotation]interface{} conflictResolvers []resolver.ConflictResolver + depsCleaners []resolver.DepsCleaner } // newScalaConfig initializes a new Config. @@ -121,6 +124,9 @@ func (c *Config) clone(config *config.Config, rel string) *Config { if c.conflictResolvers != nil { clone.conflictResolvers = c.conflictResolvers[:] } + if c.depsCleaners != nil { + clone.depsCleaners = c.depsCleaners[:] + } if c.resolveFileSymbolNames != nil { clone.resolveFileSymbolNames = c.resolveFileSymbolNames[:] } @@ -194,6 +200,10 @@ func (c *Config) ParseDirectives(directives []rule.Directive) (err error) { if err := c.parseResolveConflictsDirective(d); err != nil { return err } + case scalaDepsCleanerDirective: + if err := c.parseScalaDepsCleanerDirective(d); err != nil { + return err + } case scalaDebugDirective: if err := c.parseScalaAnnotation(d); err != nil { return err @@ -327,6 +337,31 @@ func (c *Config) parseResolveConflictsDirective(d rule.Directive) error { return nil } +func (c *Config) parseScalaDepsCleanerDirective(d rule.Directive) error { + for _, key := range strings.Fields(d.Value) { + intent := collections.ParseIntent(key) + if intent.Want { + resolver, ok := c.universe.GetDepsCleaner(intent.Value) + if !ok { + return fmt.Errorf("invalid directive gazelle:%s: unknown scala deps cleaner %q", d.Key, intent.Value) + } + for _, cr := range c.depsCleaners { + if cr.Name() == intent.Value { + break + } + } + c.depsCleaners = append(c.depsCleaners, resolver) + } else { + for i, cr := range c.depsCleaners { + if cr.Name() == intent.Value { + c.depsCleaners = removeDepsCleaner(c.depsCleaners, i) + } + } + } + } + return nil +} + func (c *Config) parseScalaAnnotation(d rule.Directive) error { for _, key := range strings.Fields(d.Value) { intent := collections.ParseIntent(key) @@ -635,3 +670,7 @@ func parseAnnotation(val string) debugAnnotation { func removeConflictResolver(slice []resolver.ConflictResolver, index int) []resolver.ConflictResolver { return append(slice[:index], slice[index+1:]...) } + +func removeDepsCleaner(slice []resolver.DepsCleaner, index int) []resolver.DepsCleaner { + return append(slice[:index], slice[index+1:]...) +} diff --git a/pkg/scalarule/mocks/ProviderRegistry.go b/pkg/scalarule/mocks/ProviderRegistry.go index 91c44c2..93ff78f 100644 --- a/pkg/scalarule/mocks/ProviderRegistry.go +++ b/pkg/scalarule/mocks/ProviderRegistry.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks