diff --git a/CHANGELOG.md b/CHANGELOG.md index b500f1dec9..5f50126038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,8 @@ END_UNRELEASED_TEMPLATE ### Added * (runfiles) Added a pathlib-compatible API: {obj}`Runfiles.root()` Fixes [#3296](https://github.com/bazel-contrib/rules_python/issues/3296). +* (gazelle) Support alias_kind directive. + Fixes [#3183](https://github.com/bazel-contrib/rules_python/issues/3183). * (toolchains) `3.13.12`, `3.14.3` Python toolchain from [20260325] release. * (toolchains) `3.10.20`, `3.11.15`, `3.12.13`, `3.13.13` `3.14.4`, `3.15.0a8` * Python toolchain from [20260414] release. diff --git a/gazelle/python/generate.go b/gazelle/python/generate.go index 2495c42d20..7e3cd8ad69 100644 --- a/gazelle/python/generate.go +++ b/gazelle/python/generate.go @@ -48,13 +48,21 @@ var ( buildFilenames = []string{"BUILD", "BUILD.bazel"} ) -func GetActualKindName(kind string, args language.GenerateArgs) string { - if kindOverride, ok := args.Config.KindMap[kind]; ok { - return kindOverride.KindName +// Returns the mapped kind, or kind if no mapping is configured with the map_kind directive. +func getMappedKind(c *config.Config, kind string) string { + if mapped, ok := c.KindMap[kind]; ok { + return mapped.KindName } return kind } +// kindMatches returns whether r matches the canonical Python rule kind `expected`, respecting `# gazelle:map_kind` and +// `# gazelle:alias_kind` directives in the config.Config c. +func kindMatches(c *config.Config, r *rule.Rule, expected string) bool { + kind := r.Kind() + return kind == getMappedKind(c, expected) || c.AliasMap[kind] == expected +} + func matchesAnyGlob(s string, globs []string) bool { // This function assumes that the globs have already been validated. If a glob is // invalid, it's considered a non-match and we move on to the next pattern. @@ -112,10 +120,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } } - actualPyBinaryKind := GetActualKindName(pyBinaryKind, args) - actualPyLibraryKind := GetActualKindName(pyLibraryKind, args) - actualPyTestKind := GetActualKindName(pyTestKind, args) - pythonProjectRoot := cfg.PythonProjectRoot() packageName := filepath.Base(args.Dir) @@ -283,10 +287,10 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes sort.Strings(mainFileNames) for _, filename := range mainFileNames { pyBinaryTargetName := strings.TrimSuffix(filepath.Base(filename), ".py") - if err := ensureNoCollision(args.File, pyBinaryTargetName, actualPyBinaryKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, pyBinaryTargetName, pyBinaryKind); err != nil { fqTarget := label.New("", args.Rel, pyBinaryTargetName) log.Printf("failed to generate target %q of kind %q: %v", - fqTarget.String(), actualPyBinaryKind, err) + fqTarget.String(), getMappedKind(args.Config, pyBinaryKind), err) continue } @@ -316,7 +320,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } generateEmptyLibrary := false for _, r := range args.File.Rules { - if r.Kind() == actualPyLibraryKind && r.Name() == pyLibraryTargetName { + if r.Name() == pyLibraryTargetName && kindMatches(args.Config, r, pyLibraryKind) { generateEmptyLibrary = true } } @@ -332,11 +336,11 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't // generate it correctly. - if err := ensureNoCollision(args.File, pyLibraryTargetName, actualPyLibraryKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, pyLibraryTargetName, pyLibraryKind); err != nil { fqTarget := label.New("", args.Rel, pyLibraryTargetName) err := fmt.Errorf("failed to generate target %q of kind %q: %w. "+ "Use the '# gazelle:%s' directive to change the naming convention.", - fqTarget.String(), actualPyLibraryKind, err, pythonconfig.LibraryNamingConvention) + fqTarget.String(), getMappedKind(args.Config, pyLibraryKind), err, pythonconfig.LibraryNamingConvention) collisionErrors.Add(err) } @@ -386,11 +390,11 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't // generate it correctly. - if err := ensureNoCollision(args.File, pyBinaryTargetName, actualPyBinaryKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, pyBinaryTargetName, pyBinaryKind); err != nil { fqTarget := label.New("", args.Rel, pyBinaryTargetName) err := fmt.Errorf("failed to generate target %q of kind %q: %w. "+ "Use the '# gazelle:%s' directive to change the naming convention.", - fqTarget.String(), actualPyBinaryKind, err, pythonconfig.BinaryNamingConvention) + fqTarget.String(), getMappedKind(args.Config, pyBinaryKind), err, pythonconfig.BinaryNamingConvention) collisionErrors.Add(err) } @@ -425,10 +429,10 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't // generate it correctly. - if err := ensureNoCollision(args.File, conftestTargetname, actualPyLibraryKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, conftestTargetname, pyLibraryKind); err != nil { fqTarget := label.New("", args.Rel, conftestTargetname) err := fmt.Errorf("failed to generate target %q of kind %q: %w. ", - fqTarget.String(), actualPyLibraryKind, err) + fqTarget.String(), getMappedKind(args.Config, pyLibraryKind), err) collisionErrors.Add(err) } @@ -462,11 +466,11 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't // generate it correctly. - if err := ensureNoCollision(args.File, pyTestTargetName, actualPyTestKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, pyTestTargetName, pyTestKind); err != nil { fqTarget := label.New("", args.Rel, pyTestTargetName) err := fmt.Errorf("failed to generate target %q of kind %q: %w. "+ "Use the '# gazelle:%s' directive to change the naming convention.", - fqTarget.String(), actualPyTestKind, err, pythonconfig.TestNamingConvention) + fqTarget.String(), getMappedKind(args.Config, pyTestKind), err, pythonconfig.TestNamingConvention) collisionErrors.Add(err) } @@ -575,8 +579,7 @@ func (py *Python) getRulesWithInvalidSrcs(args language.GenerateArgs, validFiles return strings.HasPrefix(src, "@") || strings.HasPrefix(src, "//") || strings.HasPrefix(src, ":") } for _, existingRule := range args.File.Rules { - actualPyBinaryKind := GetActualKindName(pyBinaryKind, args) - if existingRule.Kind() != actualPyBinaryKind { + if !kindMatches(args.Config, existingRule, pyBinaryKind) { continue } var hasValidSrcs bool @@ -652,12 +655,12 @@ func isEntrypointFile(path string) bool { } } -func ensureNoCollision(file *rule.File, targetName, kind string) error { +func ensureNoCollision(c *config.Config, file *rule.File, targetName, kind string) error { if file == nil { return nil } for _, t := range file.Rules { - if t.Name() == targetName && t.Kind() != kind { + if t.Name() == targetName && !kindMatches(c, t, kind) { return fmt.Errorf("a target of kind %q with the same name already exists", t.Kind()) } } @@ -680,7 +683,7 @@ func generateProtoLibraries(args language.GenerateArgs, cfg *pythonconfig.Config pyProtoRulesForProto := map[string]string{} if args.File != nil { for _, r := range args.File.Rules { - if r.Kind() == "py_proto_library" { + if kindMatches(args.Config, r, pyProtoLibraryKind) { pyProtoRules[r.Name()] = false protos := r.AttrStrings("deps") diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.in b/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.in new file mode 100644 index 0000000000..63ca6b805f --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.in @@ -0,0 +1,9 @@ +# gazelle:map_kind py_library my_lib :mytest.bzl +# gazelle:map_kind py_binary my_bin :mytest.bzl +# gazelle:map_kind py_test my_test :mytest.bzl + +py_library(name = "naming_convention_mapped_fail") + +py_binary(name = "naming_convention_mapped_fail_bin") + +py_test(name = "naming_convention_mapped_fail_test") diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.out b/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.out new file mode 100644 index 0000000000..63ca6b805f --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.out @@ -0,0 +1,9 @@ +# gazelle:map_kind py_library my_lib :mytest.bzl +# gazelle:map_kind py_binary my_bin :mytest.bzl +# gazelle:map_kind py_test my_test :mytest.bzl + +py_library(name = "naming_convention_mapped_fail") + +py_binary(name = "naming_convention_mapped_fail_bin") + +py_test(name = "naming_convention_mapped_fail_test") diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/README.md b/gazelle/python/testdata/naming_convention_mapped_fail/README.md new file mode 100644 index 0000000000..85a561c575 --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/README.md @@ -0,0 +1,4 @@ +# Naming convention fail with `map_kind` + +This test case asserts that collision error messages reference the mapped rule +kind over than the canonical Python kind when using `# gazelle:map_kind`. diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/WORKSPACE b/gazelle/python/testdata/naming_convention_mapped_fail/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/__init__.py b/gazelle/python/testdata/naming_convention_mapped_fail/__init__.py new file mode 100644 index 0000000000..a75c47faaf --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/__init__.py @@ -0,0 +1 @@ +# Empty test file. \ No newline at end of file diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/__main__.py b/gazelle/python/testdata/naming_convention_mapped_fail/__main__.py new file mode 100644 index 0000000000..a75c47faaf --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/__main__.py @@ -0,0 +1 @@ +# Empty test file. \ No newline at end of file diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/__test__.py b/gazelle/python/testdata/naming_convention_mapped_fail/__test__.py new file mode 100644 index 0000000000..a75c47faaf --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/__test__.py @@ -0,0 +1 @@ +# Empty test file. \ No newline at end of file diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/test.yaml b/gazelle/python/testdata/naming_convention_mapped_fail/test.yaml new file mode 100644 index 0000000000..bc6c083329 --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/test.yaml @@ -0,0 +1,21 @@ +# Copyright 2023 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. + +--- +expect: + exit_code: 1 + stderr: | + gazelle: ERROR: failed to generate target "//:naming_convention_mapped_fail" of kind "my_lib": a target of kind "py_library" with the same name already exists. Use the '# gazelle:python_library_naming_convention' directive to change the naming convention. + gazelle: ERROR: failed to generate target "//:naming_convention_mapped_fail_bin" of kind "my_bin": a target of kind "py_binary" with the same name already exists. Use the '# gazelle:python_binary_naming_convention' directive to change the naming convention. + gazelle: ERROR: failed to generate target "//:naming_convention_mapped_fail_test" of kind "my_test": a target of kind "py_test" with the same name already exists. Use the '# gazelle:python_test_naming_convention' directive to change the naming convention. diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.in b/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.in new file mode 100644 index 0000000000..06ebfbdfa3 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.in @@ -0,0 +1,16 @@ +load(":mylib.bzl", "my_py_library") + +# gazelle:alias_kind my_py_library py_library +# gazelle:map_kind py_test my_test :mytest.bzl + +my_py_library( + name = "respect_alias_and_map_kind", + srcs = ["__init__.py"], +) + +my_test( + name = "respect_alias_and_map_kind_test", + srcs = ["__test__.py"], + main = "__test__.py", + deps = [], +) diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.out b/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.out new file mode 100644 index 0000000000..6c7eee06e3 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.out @@ -0,0 +1,21 @@ +load(":mylib.bzl", "my_py_library") +load(":mytest.bzl", "my_test") + +# gazelle:alias_kind my_py_library py_library +# gazelle:map_kind py_test my_test :mytest.bzl + +my_py_library( + name = "respect_alias_and_map_kind", + srcs = [ + "__init__.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) + +my_test( + name = "respect_alias_and_map_kind_test", + srcs = ["__test__.py"], + main = "__test__.py", + deps = [":respect_alias_and_map_kind"], +) diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/README.md b/gazelle/python/testdata/respect_alias_and_map_kind/README.md new file mode 100644 index 0000000000..5e5ca20d27 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/README.md @@ -0,0 +1,4 @@ +# Respect `alias_kind` and `map_kind` + +This test case asserts that `# gazelle:alias_kind` and `# gazelle:map_kind` are +both respected as the expected kind at once and do not collide. diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/WORKSPACE b/gazelle/python/testdata/respect_alias_and_map_kind/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/__init__.py b/gazelle/python/testdata/respect_alias_and_map_kind/__init__.py new file mode 100644 index 0000000000..b274b0d921 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2023 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. + +from foo import foo + +_ = foo diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/__test__.py b/gazelle/python/testdata/respect_alias_and_map_kind/__test__.py new file mode 100644 index 0000000000..2b180a5f53 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/__test__.py @@ -0,0 +1,26 @@ +# Copyright 2023 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. + +import unittest + +from __init__ import foo + + +class FooTest(unittest.TestCase): + def test_foo(self): + self.assertEqual("foo", foo()) + + +if __name__ == "__main__": + unittest.main() diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/foo.py b/gazelle/python/testdata/respect_alias_and_map_kind/foo.py new file mode 100644 index 0000000000..3f049df738 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/foo.py @@ -0,0 +1,17 @@ +# Copyright 2023 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. + + +def foo(): + return "foo" diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/test.yaml b/gazelle/python/testdata/respect_alias_and_map_kind/test.yaml new file mode 100644 index 0000000000..2410223e59 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/test.yaml @@ -0,0 +1,17 @@ +# Copyright 2023 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. + +--- +expect: + exit_code: 0 diff --git a/gazelle/python/testdata/respect_alias_kind/BUILD.in b/gazelle/python/testdata/respect_alias_kind/BUILD.in new file mode 100644 index 0000000000..3dc5ca7151 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/BUILD.in @@ -0,0 +1,8 @@ +load(":mylib.bzl", "my_py_library") + +# gazelle:alias_kind my_py_library py_library + +my_py_library( + name = "respect_alias_kind", + srcs = ["__init__.py"], +) diff --git a/gazelle/python/testdata/respect_alias_kind/BUILD.out b/gazelle/python/testdata/respect_alias_kind/BUILD.out new file mode 100644 index 0000000000..9e65a95923 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/BUILD.out @@ -0,0 +1,12 @@ +load(":mylib.bzl", "my_py_library") + +# gazelle:alias_kind my_py_library py_library + +my_py_library( + name = "respect_alias_kind", + srcs = [ + "__init__.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/respect_alias_kind/README.md b/gazelle/python/testdata/respect_alias_kind/README.md new file mode 100644 index 0000000000..d0d8702106 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/README.md @@ -0,0 +1,4 @@ +# Respect `alias_kind` + +This test case asserts that Gazelle updates the existing wrapper-macro rule +declared with `# gazelle:alias_kind` in place instead of reporting a collision. diff --git a/gazelle/python/testdata/respect_alias_kind/WORKSPACE b/gazelle/python/testdata/respect_alias_kind/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/respect_alias_kind/__init__.py b/gazelle/python/testdata/respect_alias_kind/__init__.py new file mode 100644 index 0000000000..b274b0d921 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2023 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. + +from foo import foo + +_ = foo diff --git a/gazelle/python/testdata/respect_alias_kind/foo.py b/gazelle/python/testdata/respect_alias_kind/foo.py new file mode 100644 index 0000000000..3f049df738 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/foo.py @@ -0,0 +1,17 @@ +# Copyright 2023 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. + + +def foo(): + return "foo" diff --git a/gazelle/python/testdata/respect_alias_kind/test.yaml b/gazelle/python/testdata/respect_alias_kind/test.yaml new file mode 100644 index 0000000000..2410223e59 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/test.yaml @@ -0,0 +1,17 @@ +# Copyright 2023 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. + +--- +expect: + exit_code: 0