diff --git a/README.md b/README.md index a9f31a5..e27a635 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,17 @@ If you have custom implementations of the rules generated by this extension, you # gazelle:map_kind ts_project ts_project @my_local_repo ``` +`map_kind` works for every kind this extension generates, including the test rule. For +example, to emit `vitest_test` rules (that run Vitest) instead of `jest_test` while keeping +full automatic dependency management: + +```starlark +# gazelle:map_kind jest_test vitest_test //build:vitest_test.bzl +``` + +Generated test targets are then named `vitest_test`, loaded from your `.bzl` file, and still +get their `srcs`, `deps`, `data`, and `config` populated from the test file's imports. + ## Directives Gazelle can be configured with _directives_, which are written as top-level diff --git a/gazelle/generate.go b/gazelle/generate.go index 85d15cd..6ac0d12 100644 --- a/gazelle/generate.go +++ b/gazelle/generate.go @@ -84,13 +84,24 @@ func (lang *JS) Loads() []rule.LoadInfo { } } -func getKind(c *config.Config, kindName string) string { - // Extract kind_name from KindMap - if kind, ok := c.KindMap[kindName]; ok { - return kind.KindName - +// unmapKind reverses "# gazelle:map_kind" renames to recover the builtin kind +// of a rule read from disk, following chains (identity if unmapped). +func unmapKind(c *config.Config, kindName string) string { + seen := map[string]bool{kindName: true} + for { + from := "" + for _, mapped := range c.KindMap { + if mapped.KindName == kindName { + from = mapped.FromKind + break + } + } + if from == "" || seen[from] { + return kindName + } + seen[from] = true + kindName = from } - return kindName } // GenerateRules extracts build metadata from source files in a directory. @@ -228,7 +239,7 @@ func (lang *JS) GenerateRules(args language.GenerateArgs) language.GenerateResul generatedImports = append(generatedImports, generatedTSImports...) existingRules := lang.readExistingRules(args, true) - lang.pruneManagedRules(existingRules, generatedRules) + lang.pruneManagedRules(args.Config, existingRules, generatedRules) return language.GenerateResult{ Gen: generatedRules, @@ -362,7 +373,7 @@ func readFileAndParse(filePath string, rel string) (*imports, int) { func (lang *JS) genPkgRule(args language.GenerateArgs, jsConfig *JsConfig) *rule.Rule { for _, baseName := range args.RegularFiles { if baseName == "package.json" { - r := rule.NewRule(getKind(args.Config, "js_library"), "package_json") + r := rule.NewRule("js_library", "package_json") r.SetAttr("srcs", []string{baseName}) if len(jsConfig.Visibility.Labels) > 0 { r.SetAttr("visibility", jsConfig.Visibility.Labels) @@ -386,7 +397,7 @@ func (lang *JS) genJestTest(args language.GenerateArgs, jsConfig *JsConfig, jest ruleName := strings.TrimSuffix(baseName, extension) + ".test" r := rule.NewRule( - getKind(args.Config, "jest_test"), + "jest_test", ruleName, ) r.SetAttr("srcs", []string{baseName}) @@ -421,7 +432,7 @@ func (lang *JS) genJestTest(args language.GenerateArgs, jsConfig *JsConfig, jest pkgName := PkgName(args.Rel) ruleName := fmt.Sprintf("%s_test", pkgName) r := rule.NewRule( - getKind(args.Config, "jest_test"), + "jest_test", ruleName, ) @@ -492,7 +503,8 @@ func (lang *JS) genRules(args language.GenerateArgs, jsConfig *JsConfig, isBarre // add as a folder for _, existingRule := range lang.readExistingRules(args, false) { // Look for existing rules with the same name, but different kind - if existingRule.Name() == name && existingRule.Kind() != getKind(args.Config, "ts_project") && existingRule.Kind() != getKind(args.Config, "js_library") { + existingKind := unmapKind(args.Config, existingRule.Kind()) + if existingRule.Name() == name && existingKind != "ts_project" && existingKind != "js_library" { if kind == "ts_project" { name = name + "_ts" } else { @@ -504,7 +516,7 @@ func (lang *JS) genRules(args language.GenerateArgs, jsConfig *JsConfig, isBarre folderImports, folderRule := lang.makeFolderRule(moduleRuleArgs{ pkgName: name, cwd: args.Rel, - ruleType: getKind(args.Config, kind), + ruleType: kind, srcs: sources, imports: imports, }, jsConfig) @@ -522,7 +534,7 @@ func (lang *JS) genRules(args language.GenerateArgs, jsConfig *JsConfig, isBarre moduleImports, moduleRules := lang.makeModuleRules(moduleRuleArgs{ pkgName: name, cwd: args.Rel, - ruleType: getKind(args.Config, kind), + ruleType: kind, srcs: sources, imports: imports, }, jsConfig) @@ -542,7 +554,7 @@ func (lang *JS) genRules(args language.GenerateArgs, jsConfig *JsConfig, isBarre } else { // add as singletons singletonRules := lang.makeRules(ruleArgs{ - ruleType: getKind(args.Config, kind), + ruleType: kind, srcs: sources, trimExt: true, }, jsConfig) @@ -766,7 +778,7 @@ func (lang *JS) genWebAssets(args language.GenerateArgs, webAssetsSet map[string if jsConfig.CollectWebAssets { // aggregate rule name := "assets" - r := rule.NewRule(getKind(args.Config, "web_assets"), name) + r := rule.NewRule("web_assets", name) r.SetAttr("srcs", webAssets) if len(jsConfig.Visibility.Labels) > 0 { r.SetAttr("visibility", jsConfig.Visibility.Labels) @@ -782,7 +794,7 @@ func (lang *JS) genWebAssets(args language.GenerateArgs, webAssetsSet map[string } else { // add as singletons rules := lang.makeRules(ruleArgs{ - ruleType: getKind(args.Config, "web_assets"), + ruleType: "web_assets", srcs: webAssets, trimExt: false, //shadow the original file name }, jsConfig) @@ -812,7 +824,7 @@ func (lang *JS) genAllAssets(args language.GenerateArgs, isJSRoot bool, jsConfig JSRootDeps = append(JSRootDeps, fqName) } name := "all_assets" - r := rule.NewRule(getKind(args.Config, "web_assets"), name) + r := rule.NewRule("web_assets", name) r.SetAttr("srcs", JSRootDeps) generatedRules = append(generatedRules, r) @@ -829,7 +841,7 @@ func (lang *JS) genCollectedTargetsRule(args language.GenerateArgs, jsConfig *Js if jsConfig.CollectTargets != "" { // Add an empty `js_library` rule. This will be given `deps` later in resolve.go - r := rule.NewRule(getKind(args.Config, "js_library"), jsConfig.CollectTargets) + r := rule.NewRule("js_library", jsConfig.CollectTargets) if len(jsConfig.Visibility.Labels) > 0 { r.SetAttr("visibility", jsConfig.Visibility.Labels) @@ -849,7 +861,7 @@ func (lang *JS) readExistingRules(args language.GenerateArgs, managedOnly bool) // For each existing rule for _, r := range BUILD.Rules { if managedOnly { - if _, ok := managedRulesSet[r.Kind()]; !ok { + if _, ok := managedRulesSet[unmapKind(args.Config, r.Kind())]; !ok { // skip unmanaged rules continue } @@ -860,7 +872,7 @@ func (lang *JS) readExistingRules(args language.GenerateArgs, managedOnly bool) return existingRules } -func (lang *JS) pruneManagedRules(existingRules map[string]*rule.Rule, generatedRules []*rule.Rule) { +func (lang *JS) pruneManagedRules(c *config.Config, existingRules map[string]*rule.Rule, generatedRules []*rule.Rule) { // Generate a list of rules that may be deleted and mark them for deletion // This is generated from existing rules that are managed by gazelle // that didn't get generated this run @@ -869,19 +881,19 @@ func (lang *JS) pruneManagedRules(existingRules map[string]*rule.Rule, generated deleteRulesSet := make(map[string]*rule.Rule) for _, existingRule := range existingRules { // use kind/name to enable deletion of old rules when a new rule would use the same name - key := fmt.Sprintf("%s/%s", existingRule.Kind(), existingRule.Name()) + key := fmt.Sprintf("%s/%s", unmapKind(c, existingRule.Kind()), existingRule.Name()) deleteRulesSet[key] = existingRule } // Prune generated rules for _, generatedRule := range generatedRules { - key := fmt.Sprintf("%s/%s", generatedRule.Kind(), generatedRule.Name()) + key := fmt.Sprintf("%s/%s", unmapKind(c, generatedRule.Kind()), generatedRule.Name()) delete(deleteRulesSet, key) } for _, r := range deleteRulesSet { // Is this rule managed by Gazelle? - if _, ok := managedRulesSet[r.Kind()]; ok { + if _, ok := managedRulesSet[unmapKind(c, r.Kind())]; ok { // It is managed, and wasn't generated, so delete it r.Delete() } diff --git a/gazelle/generate_test.go b/gazelle/generate_test.go index f194fcd..d7df0fa 100644 --- a/gazelle/generate_test.go +++ b/gazelle/generate_test.go @@ -18,6 +18,8 @@ package js import ( "testing" + + "github.com/bazelbuild/bazel-gazelle/config" ) func TestPattern(t *testing.T) { @@ -26,3 +28,36 @@ func TestPattern(t *testing.T) { t.FailNow() } } + +func TestUnmapKind(t *testing.T) { + std := map[string]config.MappedKind{ + "jest_test": {FromKind: "jest_test", KindName: "vitest_test"}, + "ts_project": {FromKind: "ts_project", KindName: "mid"}, + "mid": {FromKind: "mid", KindName: "outer"}, + "web_assets": {FromKind: "web_assets", KindName: "web_assets"}, + } + cycle := map[string]config.MappedKind{ + "x": {FromKind: "x", KindName: "y"}, + "y": {FromKind: "y", KindName: "x"}, + } + for _, tc := range []struct { + desc string + kindMap map[string]config.MappedKind + in, want string + }{ + {"single step reverse", std, "vitest_test", "jest_test"}, + {"chained reverse to builtin", std, "outer", "ts_project"}, + {"partial chain", std, "mid", "ts_project"}, + {"self map is identity", std, "web_assets", "web_assets"}, + {"unmapped builtin unchanged", std, "jest_test", "jest_test"}, + {"completely unmapped", std, "js_library", "js_library"}, + {"cycle terminates", cycle, "x", "y"}, + } { + t.Run(tc.desc, func(t *testing.T) { + c := &config.Config{KindMap: tc.kindMap} + if got := unmapKind(c, tc.in); got != tc.want { + t.Errorf("unmapKind(%q) = %q, want %q", tc.in, got, tc.want) + } + }) + } +} diff --git a/gazelle/resolve.go b/gazelle/resolve.go index f4744ef..a475a22 100644 --- a/gazelle/resolve.go +++ b/gazelle/resolve.go @@ -112,7 +112,7 @@ func (lang *JS) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve. } // modules can be resolved via the directory containing them - if (isBarrel || jsConfig.CollectAll) && r.Kind() != getKind(c, "jest_test") { + if (isBarrel || jsConfig.CollectAll) && r.Kind() != "jest_test" { importSpecs = append(importSpecs, resolve.ImportSpec{ Lang: lang.Name(), Imp: f.Pkg, @@ -120,7 +120,7 @@ func (lang *JS) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve. } // Any subfolders could be used to depend on this rule - folderImports := jsConfig.CollectAll && (r.Kind() == getKind(c, "ts_project") || r.Kind() == getKind(c, "js_library")) + folderImports := jsConfig.CollectAll && (r.Kind() == "ts_project" || r.Kind() == "js_library") if folderImports { base := filepath.Dir(f.Path) subDirectories := make(map[string]bool) @@ -256,7 +256,7 @@ func (lang *JS) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.Remote } // Add in additional jest dependencies - if r.Kind() == getKind(c, "jest_test") { + if r.Kind() == "jest_test" { // All deps are also data for jest_test rules. for name := range depSet { dataSet[name] = true diff --git a/tests/BUILD b/tests/BUILD index 6a1abd5..aa4f754 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -59,6 +59,7 @@ go_binary( "jest_mock", "jsx_conversion", "lookup_types", + "map_kind_test", "module_self_import", "react_example", "simple_barrel", diff --git a/tests/map_kind_test/BUILD.in b/tests/map_kind_test/BUILD.in new file mode 100644 index 0000000..e3374c9 --- /dev/null +++ b/tests/map_kind_test/BUILD.in @@ -0,0 +1,5 @@ +# gazelle:js_root +# gazelle:js_package_file package.json :node_modules +# gazelle:js_jest_config :jest.config + +# gazelle:map_kind jest_test vitest_test //build:vitest_test.bzl diff --git a/tests/map_kind_test/BUILD.out b/tests/map_kind_test/BUILD.out new file mode 100644 index 0000000..50c46d9 --- /dev/null +++ b/tests/map_kind_test/BUILD.out @@ -0,0 +1,24 @@ +load("@aspect_rules_js//js:defs.bzl", "js_library") +load("//build:vitest_test.bzl", "vitest_test") + +# gazelle:js_root +# gazelle:js_package_file package.json :node_modules +# gazelle:js_jest_config :jest.config + +# gazelle:map_kind jest_test vitest_test //build:vitest_test.bzl + +js_library( + name = "package_json", + srcs = ["package.json"], +) + +vitest_test( + name = "foo.test", + srcs = ["foo.test.ts"], + config = "//:jest.config", + data = [ + "//:node_modules/lodash", + "//:package_json", + ], + deps = ["//:node_modules/lodash"], +) diff --git a/tests/map_kind_test/WORKSPACE b/tests/map_kind_test/WORKSPACE new file mode 100644 index 0000000..e69de29 diff --git a/tests/map_kind_test/foo.test.ts b/tests/map_kind_test/foo.test.ts new file mode 100644 index 0000000..f8a58a2 --- /dev/null +++ b/tests/map_kind_test/foo.test.ts @@ -0,0 +1,5 @@ +import { isEqual } from 'lodash' + +it('works', () => { + isEqual(1, 1) +}) diff --git a/tests/map_kind_test/package.json b/tests/map_kind_test/package.json new file mode 100644 index 0000000..e76b4b8 --- /dev/null +++ b/tests/map_kind_test/package.json @@ -0,0 +1,8 @@ +{ + "name": "map_kind_test", + "description": "A test case", + "version": "0.0.0", + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/tests/map_kind_test/rerun/BUILD.in b/tests/map_kind_test/rerun/BUILD.in new file mode 100644 index 0000000..513e122 --- /dev/null +++ b/tests/map_kind_test/rerun/BUILD.in @@ -0,0 +1,6 @@ +load("//build:vitest_test.bzl", "vitest_test") + +vitest_test( + name = "bar.test", + srcs = ["bar.test.ts"], +) diff --git a/tests/map_kind_test/rerun/BUILD.out b/tests/map_kind_test/rerun/BUILD.out new file mode 100644 index 0000000..902c466 --- /dev/null +++ b/tests/map_kind_test/rerun/BUILD.out @@ -0,0 +1,12 @@ +load("//build:vitest_test.bzl", "vitest_test") + +vitest_test( + name = "bar.test", + srcs = ["bar.test.ts"], + config = "//:jest.config", + data = [ + "//:node_modules/lodash", + "//:package_json", + ], + deps = ["//:node_modules/lodash"], +) diff --git a/tests/map_kind_test/rerun/bar.test.ts b/tests/map_kind_test/rerun/bar.test.ts new file mode 100644 index 0000000..9662d93 --- /dev/null +++ b/tests/map_kind_test/rerun/bar.test.ts @@ -0,0 +1,5 @@ +import { isEqual } from 'lodash' + +it('still works after re-run', () => { + isEqual(2, 2) +})