From b7c6292d99041546c2abb9f6e12b9e4a9c4c3481 Mon Sep 17 00:00:00 2001 From: Simon Mavi Stewart Date: Thu, 27 Nov 2025 15:13:48 +0000 Subject: [PATCH 1/3] feat(java/gazelle): scaffold class-level dependency resolution Currently, the Java Gazelle plugin relies solely on package-level indexing for dependency resolution. This is inefficient for split packages in large corporate repositories, often requiring manual #gazelle:resolve directives. This change introduces the architecture for class-level resolution: Updates the resolution input pipeline to carry class-level import data alongside package data. Extends the Maven resolver to support querying by class name (with package fallback). Updates the resolution logic to prioritize precise class matches over broad package matches. This structure prepares the plugin for future improvements where class indices can be populated from jar files or external services, enabling automatic and precise resolution for split packages. --- java/gazelle/constants.go | 1 + java/gazelle/generate.go | 43 ++++++--- java/gazelle/generate_test.go | 2 +- java/gazelle/private/maven/config.go | 9 ++ java/gazelle/private/maven/resolver.go | 27 +++++- java/gazelle/private/types/types.go | 1 + java/gazelle/resolve.go | 88 +++++++++++++++++++ java/gazelle/resolve_test.go | 8 ++ .../testdata/kt_split_package/BUILD.in | 1 + .../testdata/kt_split_package/WORKSPACE | 1 + .../kt_split_package/app/src/main/BUILD.in | 0 .../kt_split_package/app/src/main/BUILD.out | 11 +++ .../kt_split_package/app/src/main/Main.kt | 9 ++ .../kt_split_package/maven_install.json | 1 + .../kt_split_package/one/src/BUILD.in | 0 .../kt_split_package/one/src/BUILD.out | 7 ++ .../kt_split_package/one/src/ClassA.kt | 3 + .../kt_split_package/two/src/BUILD.in | 0 .../kt_split_package/two/src/BUILD.out | 7 ++ .../kt_split_package/two/src/ClassB.kt | 3 + java/gazelle/testdata/split_package/BUILD.in | 0 java/gazelle/testdata/split_package/WORKSPACE | 1 + .../split_package/app/src/java/BUILD.in | 0 .../split_package/app/src/java/BUILD.out | 11 +++ .../split_package/app/src/java/Main.java | 9 ++ .../testdata/split_package/maven_install.json | 1 + .../split_package/one/src/java/BUILD.in | 0 .../split_package/one/src/java/BUILD.out | 7 ++ .../split_package/one/src/java/ClassA.java | 3 + .../split_package/two/src/java/BUILD.in | 0 .../split_package/two/src/java/BUILD.out | 7 ++ .../split_package/two/src/java/ClassB.java | 3 + .../generators/ClasspathParser.java | 14 +++ 33 files changed, 260 insertions(+), 18 deletions(-) create mode 100644 java/gazelle/testdata/kt_split_package/BUILD.in create mode 100644 java/gazelle/testdata/kt_split_package/WORKSPACE create mode 100644 java/gazelle/testdata/kt_split_package/app/src/main/BUILD.in create mode 100644 java/gazelle/testdata/kt_split_package/app/src/main/BUILD.out create mode 100644 java/gazelle/testdata/kt_split_package/app/src/main/Main.kt create mode 100644 java/gazelle/testdata/kt_split_package/maven_install.json create mode 100644 java/gazelle/testdata/kt_split_package/one/src/BUILD.in create mode 100644 java/gazelle/testdata/kt_split_package/one/src/BUILD.out create mode 100644 java/gazelle/testdata/kt_split_package/one/src/ClassA.kt create mode 100644 java/gazelle/testdata/kt_split_package/two/src/BUILD.in create mode 100644 java/gazelle/testdata/kt_split_package/two/src/BUILD.out create mode 100644 java/gazelle/testdata/kt_split_package/two/src/ClassB.kt create mode 100644 java/gazelle/testdata/split_package/BUILD.in create mode 100644 java/gazelle/testdata/split_package/WORKSPACE create mode 100644 java/gazelle/testdata/split_package/app/src/java/BUILD.in create mode 100644 java/gazelle/testdata/split_package/app/src/java/BUILD.out create mode 100644 java/gazelle/testdata/split_package/app/src/java/Main.java create mode 100644 java/gazelle/testdata/split_package/maven_install.json create mode 100644 java/gazelle/testdata/split_package/one/src/java/BUILD.in create mode 100644 java/gazelle/testdata/split_package/one/src/java/BUILD.out create mode 100644 java/gazelle/testdata/split_package/one/src/java/ClassA.java create mode 100644 java/gazelle/testdata/split_package/two/src/java/BUILD.in create mode 100644 java/gazelle/testdata/split_package/two/src/java/BUILD.out create mode 100644 java/gazelle/testdata/split_package/two/src/java/ClassB.java diff --git a/java/gazelle/constants.go b/java/gazelle/constants.go index 5a8f1791..a20f0b3a 100644 --- a/java/gazelle/constants.go +++ b/java/gazelle/constants.go @@ -4,3 +4,4 @@ package gazelle // rules. This attribute contains a list of package names (as type types.PackageName) it can be imported // for. Note that the Java plugin currently uses package names, not classes, as its importable unit. const packagesKey = "_java_packages" +const classesKey = "_java_classes" diff --git a/java/gazelle/generate.go b/java/gazelle/generate.go index 0a30ab17..796dc756 100644 --- a/java/gazelle/generate.go +++ b/java/gazelle/generate.go @@ -29,7 +29,13 @@ type javaFile struct { } func (jf *javaFile) ClassName() *types.ClassName { - className := types.NewClassName(jf.pkg, strings.TrimSuffix(filepath.Base(jf.pathRelativeToBazelWorkspaceRoot), ".java")) + name := filepath.Base(jf.pathRelativeToBazelWorkspaceRoot) + if strings.HasSuffix(name, ".java") { + name = strings.TrimSuffix(name, ".java") + } else if strings.HasSuffix(name, ".kt") { + name = strings.TrimSuffix(name, ".kt") + } + className := types.NewClassName(jf.pkg, name) return &className } @@ -155,6 +161,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes productionJavaImports := sorted_set.NewSortedSetFn([]types.PackageName{}, types.PackageNameLess) productionJavaImportedClasses := sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess) nonLocalJavaExports := sorted_set.NewSortedSetFn([]types.PackageName{}, types.PackageNameLess) + nonLocalJavaExportedClasses := sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess) // Files and imports for actual test classes. testJavaFiles := sorted_set.NewSortedSetFn([]javaFile{}, javaFileLess) @@ -180,14 +187,16 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes allPackageNames.Add(mJavaPkg.Name) if !mJavaPkg.TestPackage { - addNonLocalImportsAndExports(productionJavaImports, productionJavaImportedClasses, nonLocalJavaExports, mJavaPkg.ImportedClasses, mJavaPkg.ImportedPackagesWithoutSpecificClasses, mJavaPkg.ExportedClasses, mJavaPkg.Name, likelyLocalClassNames) + addNonLocalImportsAndExports(productionJavaImports, productionJavaImportedClasses, nonLocalJavaExports, nonLocalJavaExportedClasses, mJavaPkg.ImportedClasses, mJavaPkg.ImportedPackagesWithoutSpecificClasses, mJavaPkg.ExportedClasses, mJavaPkg.Name, likelyLocalClassNames) for _, f := range mJavaPkg.Files.SortedSlice() { productionJavaFiles.Add(filepath.Join(mRel, f)) + jf := javaFile{pathRelativeToBazelWorkspaceRoot: filepath.Join(mRel, f), pkg: mJavaPkg.Name} + nonLocalJavaExportedClasses.Add(*jf.ClassName()) } allMains.AddAll(mJavaPkg.Mains) } else { // Tests don't get to export things, as things shouldn't depend on them. - addNonLocalImportsAndExports(testJavaImports, testJavaImportedClasses, nil, mJavaPkg.ImportedClasses, mJavaPkg.ImportedPackagesWithoutSpecificClasses, mJavaPkg.ExportedClasses, mJavaPkg.Name, likelyLocalClassNames) + addNonLocalImportsAndExports(testJavaImports, testJavaImportedClasses, nil, nil, mJavaPkg.ImportedClasses, mJavaPkg.ImportedPackagesWithoutSpecificClasses, mJavaPkg.ExportedClasses, mJavaPkg.Name, likelyLocalClassNames) for _, f := range mJavaPkg.Files.SortedSlice() { path := filepath.Join(mRel, f) file := javaFile{ @@ -205,9 +214,9 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes allPackageNames.Add(javaPkg.Name) if javaPkg.TestPackage { // Tests don't get to export things, as things shouldn't depend on them. - addNonLocalImportsAndExports(testJavaImports, testJavaImportedClasses, nil, javaPkg.ImportedClasses, javaPkg.ImportedPackagesWithoutSpecificClasses, javaPkg.ExportedClasses, javaPkg.Name, likelyLocalClassNames) + addNonLocalImportsAndExports(testJavaImports, testJavaImportedClasses, nil, nil, javaPkg.ImportedClasses, javaPkg.ImportedPackagesWithoutSpecificClasses, javaPkg.ExportedClasses, javaPkg.Name, likelyLocalClassNames) } else { - addNonLocalImportsAndExports(productionJavaImports, productionJavaImportedClasses, nonLocalJavaExports, javaPkg.ImportedClasses, javaPkg.ImportedPackagesWithoutSpecificClasses, javaPkg.ExportedClasses, javaPkg.Name, likelyLocalClassNames) + addNonLocalImportsAndExports(productionJavaImports, productionJavaImportedClasses, nonLocalJavaExports, nonLocalJavaExportedClasses, javaPkg.ImportedClasses, javaPkg.ImportedPackagesWithoutSpecificClasses, javaPkg.ExportedClasses, javaPkg.Name, likelyLocalClassNames) } allMains.AddAll(javaPkg.Mains) for _, f := range srcFilenamesRelativeToPackage { @@ -220,6 +229,8 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes accumulateJavaFile(cfg, testJavaFiles, testHelperJavaFiles, separateTestJavaFiles, file, javaPkg.PerClassMetadata, log) } else { productionJavaFiles.Add(path) + jf := javaFile{pathRelativeToBazelWorkspaceRoot: path, pkg: javaPkg.Name} + nonLocalJavaExportedClasses.Add(*jf.ClassName()) } } for _, annotationClass := range javaPkg.AllAnnotations().SortedSlice() { @@ -335,7 +346,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes } } - l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), productionJavaFiles.SortedSlice(), resourcesDirectRef, resourcesRuntimeDep, allPackageNames, nonLocalProductionJavaImports, nonLocalProductionJavaImportedClasses, nonLocalJavaExports, annotationProcessorClasses, false, javaLibraryKind, &res, cfg, args.Config.RepoName) + l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), productionJavaFiles.SortedSlice(), resourcesDirectRef, resourcesRuntimeDep, allPackageNames, nonLocalProductionJavaImports, nonLocalProductionJavaImportedClasses, nonLocalJavaExports, nonLocalJavaExportedClasses, annotationProcessorClasses, false, javaLibraryKind, &res, cfg, args.Config.RepoName) } if cfg.GenerateBinary() { @@ -345,6 +356,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes // We add special packages to point to testonly libraries which - this accumulates them, // as well as the existing java imports of tests. testJavaImportsWithHelpers := testJavaImports.Clone() + testJavaImportedClassesWithHelpers := testJavaImportedClasses.Clone() if testHelperJavaFiles.Len() > 0 { // Suites generate their own helper library. @@ -355,10 +367,11 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes for _, tf := range testHelperJavaFiles.SortedSlice() { packages.Add(tf.pkg) testJavaImportsWithHelpers.Add(tf.pkg) + testJavaImportedClassesWithHelpers.Add(*tf.ClassName()) srcs = append(srcs, tf.pathRelativeToBazelWorkspaceRoot) } // Test helper libraries typically don't have resources - l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), srcs, "", "", packages, testJavaImports, testJavaImportedClasses, nonLocalJavaExports, annotationProcessorClasses, true, javaLibraryKind, &res, cfg, args.Config.RepoName) + l.generateJavaLibrary(args.File, args.Rel, filepath.Base(args.Rel), srcs, "", "", packages, testJavaImports, testJavaImportedClasses, nonLocalJavaExports, nonLocalJavaExportedClasses, annotationProcessorClasses, true, javaLibraryKind, &res, cfg, args.Config.RepoName) } } @@ -370,7 +383,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes case "file": for _, tf := range testJavaFiles.SortedSlice() { separateJavaTestReasons := separateTestJavaFiles[tf] - l.generateJavaTest(args.File, args.Rel, cfg.MavenRepositoryName(), tf, isModule, testJavaImportsWithHelpers, testJavaImportedClasses, annotationProcessorClasses, nil, separateJavaTestReasons.wrapper, separateJavaTestReasons.attributes, &res) + l.generateJavaTest(args.File, args.Rel, cfg.MavenRepositoryName(), tf, isModule, testJavaImportsWithHelpers, testJavaImportedClassesWithHelpers, annotationProcessorClasses, nil, separateJavaTestReasons.wrapper, separateJavaTestReasons.attributes, &res) } case "suite": @@ -398,7 +411,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes packageNames, cfg.MavenRepositoryName(), testJavaImportsWithHelpers, - testJavaImportedClasses, + testJavaImportedClassesWithHelpers, annotationProcessorClasses, cfg.GetCustomJavaTestFileSuffixes(), testHelperJavaFiles.Len() > 0, @@ -416,7 +429,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes testHelperDep = ptr(testHelperLibname(suiteName)) } separateJavaTestReasons := separateTestJavaFiles[src] - l.generateJavaTest(args.File, args.Rel, cfg.MavenRepositoryName(), src, isModule, testJavaImportsWithHelpers, testJavaImportedClasses, annotationProcessorClasses, testHelperDep, separateJavaTestReasons.wrapper, separateJavaTestReasons.attributes, &res) + l.generateJavaTest(args.File, args.Rel, cfg.MavenRepositoryName(), src, isModule, testJavaImportsWithHelpers, testJavaImportedClassesWithHelpers, annotationProcessorClasses, testHelperDep, separateJavaTestReasons.wrapper, separateJavaTestReasons.attributes, &res) } } } @@ -521,11 +534,11 @@ func generateProtoLibraries(args language.GenerateArgs, log zerolog.Logger, res // We exclude intra-target imports because otherwise we'd get self-dependencies come resolve time. // toExports is optional and may be nil. All other parameters are required and must be non-nil. -func addNonLocalImportsAndExports(toImports *sorted_set.SortedSet[types.PackageName], toImportedClasses *sorted_set.SortedSet[types.ClassName], toExports *sorted_set.SortedSet[types.PackageName], fromImportedClasses *sorted_set.SortedSet[types.ClassName], fromPackages *sorted_set.SortedSet[types.PackageName], fromExportedClasses *sorted_set.SortedSet[types.ClassName], pkg types.PackageName, localClasses *sorted_set.SortedSet[string]) { +func addNonLocalImportsAndExports(toImports *sorted_set.SortedSet[types.PackageName], toImportedClasses *sorted_set.SortedSet[types.ClassName], toExports *sorted_set.SortedSet[types.PackageName], toExportedClasses *sorted_set.SortedSet[types.ClassName], fromImportedClasses *sorted_set.SortedSet[types.ClassName], fromPackages *sorted_set.SortedSet[types.PackageName], fromExportedClasses *sorted_set.SortedSet[types.ClassName], pkg types.PackageName, localClasses *sorted_set.SortedSet[string]) { toImports.AddAll(fromPackages) addFilteringOutOwnPackage(toImports, toImportedClasses, fromImportedClasses, pkg, localClasses) if toExports != nil { - addFilteringOutOwnPackage(toExports, nil, fromExportedClasses, pkg, localClasses) + addFilteringOutOwnPackage(toExports, toExportedClasses, fromExportedClasses, pkg, localClasses) } } @@ -597,7 +610,7 @@ func accumulateJavaFile(cfg *javaconfig.Config, testJavaFiles, testHelperJavaFil } } -func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBazelWorkspace, name string, srcsRelativeToBazelWorkspace []string, resourcesDirectRef string, resourcesRuntimeDep string, packages, imports *sorted_set.SortedSet[types.PackageName], importedClasses *sorted_set.SortedSet[types.ClassName], exports *sorted_set.SortedSet[types.PackageName], annotationProcessorClasses *sorted_set.SortedSet[types.ClassName], testonly bool, javaLibraryRuleKind string, res *language.GenerateResult, cfg *javaconfig.Config, repoName string) { +func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBazelWorkspace, name string, srcsRelativeToBazelWorkspace []string, resourcesDirectRef string, resourcesRuntimeDep string, packages, imports *sorted_set.SortedSet[types.PackageName], importedClasses *sorted_set.SortedSet[types.ClassName], exports *sorted_set.SortedSet[types.PackageName], exportedClasses *sorted_set.SortedSet[types.ClassName], annotationProcessorClasses *sorted_set.SortedSet[types.ClassName], testonly bool, javaLibraryRuleKind string, res *language.GenerateResult, cfg *javaconfig.Config, repoName string) { r := rule.NewRule(javaLibraryRuleKind, name) srcs := make([]string, 0, len(srcsRelativeToBazelWorkspace)) @@ -641,6 +654,9 @@ func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBa resolvablePackages = append(resolvablePackages, *types.NewResolvableJavaPackage(pkg, testonly, false)) } r.SetPrivateAttr(packagesKey, resolvablePackages) + if exportedClasses != nil { + r.SetPrivateAttr(classesKey, exportedClasses.SortedSlice()) + } res.Gen = append(res.Gen, r) resolveInput := types.ResolveInput{ @@ -648,6 +664,7 @@ func (l javaLang) generateJavaLibrary(file *rule.File, pathToPackageRelativeToBa ImportedPackageNames: imports, ImportedClasses: importedClasses, ExportedPackageNames: exports, + ExportedClassNames: exportedClasses, AnnotationProcessors: annotationProcessorClasses, } res.Imports = append(res.Imports, resolveInput) diff --git a/java/gazelle/generate_test.go b/java/gazelle/generate_test.go index b07ad0dc..78c1728e 100644 --- a/java/gazelle/generate_test.go +++ b/java/gazelle/generate_test.go @@ -309,7 +309,7 @@ func TestAddNonLocalImports(t *testing.T) { depsDst := sorted_set.NewSortedSetFn([]types.PackageName{}, types.PackageNameLess) exportsDst := sorted_set.NewSortedSetFn([]types.PackageName{}, types.PackageNameLess) - addNonLocalImportsAndExports(depsDst, nil, exportsDst, src, sorted_set.NewSortedSetFn[types.PackageName]([]types.PackageName{}, types.PackageNameLess), sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess), types.NewPackageName("com.example.a.b"), sorted_set.NewSortedSet([]string{"Foo", "Bar"})) + addNonLocalImportsAndExports(depsDst, nil, exportsDst, nil, src, sorted_set.NewSortedSetFn[types.PackageName]([]types.PackageName{}, types.PackageNameLess), sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess), types.NewPackageName("com.example.a.b"), sorted_set.NewSortedSet([]string{"Foo", "Bar"})) want := stringsToPackageNames([]string{ "com.another.a.b", diff --git a/java/gazelle/private/maven/config.go b/java/gazelle/private/maven/config.go index 08937f5c..0d384fed 100644 --- a/java/gazelle/private/maven/config.go +++ b/java/gazelle/private/maven/config.go @@ -10,6 +10,7 @@ type lockFile interface { ListDependencies() []string GetDependencyCoordinates(name string) string ListDependencyPackages(name string) []string + ListDependencyClasses(name string) []string } type versionnedConfigFile struct { @@ -62,6 +63,10 @@ func (f *lockFileV1) ListDependencyPackages(name string) []string { panic(fmt.Sprintf("did not find package information for %s", name)) } +func (f *lockFileV1) ListDependencyClasses(name string) []string { + return nil +} + type lockFileV2 struct { AutogeneratedFileDoNotModifyThisFileManually string `json:"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY"` InputArtifactsHash int `json:"__INPUT_ARTIFACTS_HASH"` @@ -89,6 +94,10 @@ func (f *lockFileV2) ListDependencyPackages(name string) []string { return f.Packages[name] } +func (f *lockFileV2) ListDependencyClasses(name string) []string { + return nil +} + type lockFileV2_Artifact struct { Shasums map[string]string `json:"shasums"` Version string `json:"version"` diff --git a/java/gazelle/private/maven/resolver.go b/java/gazelle/private/maven/resolver.go index 4f97e3c9..28fd10c9 100644 --- a/java/gazelle/private/maven/resolver.go +++ b/java/gazelle/private/maven/resolver.go @@ -32,19 +32,22 @@ func (e *MultipleExternalImportsError) Error() string { type Resolver interface { Resolve(pkg types.PackageName, excludedArtifacts map[string]struct{}, mavenRepositoryName string) (label.Label, error) + ResolveClass(className types.ClassName, excludedArtifacts map[string]struct{}, mavenRepositoryName string) (label.Label, error) } // resolver finds Maven provided packages by reading the maven_install.json // file from rules_jvm_external. type resolver struct { - data *multiset.StringMultiSet - logger zerolog.Logger + data *multiset.StringMultiSet + classIndex map[string]string + logger zerolog.Logger } func NewResolver(installFile string, logger zerolog.Logger) (Resolver, error) { r := resolver{ - data: multiset.NewStringMultiSet(), - logger: logger.With().Str("_c", "maven-resolver").Logger(), + data: multiset.NewStringMultiSet(), + classIndex: make(map[string]string), + logger: logger.With().Str("_c", "maven-resolver").Logger(), } c, err := loadConfiguration(installFile) @@ -65,6 +68,9 @@ func NewResolver(installFile string, logger zerolog.Logger) (Resolver, error) { for _, pkg := range c.ListDependencyPackages(depName) { r.data.Add(pkg, coords.ArtifactString()) } + for _, class := range c.ListDependencyClasses(depName) { + r.classIndex[class] = coords.ArtifactString() + } } return &r, nil @@ -105,6 +111,19 @@ func (r *resolver) Resolve(pkg types.PackageName, excludedArtifacts map[string]s } } +func (r *resolver) ResolveClass(className types.ClassName, excludedArtifacts map[string]struct{}, mavenRepositoryName string) (label.Label, error) { + artifact, found := r.classIndex[className.FullyQualifiedClassName()] + if !found { + return label.NoLabel, nil + } + + if _, excluded := excludedArtifacts[LabelFromArtifact(mavenRepositoryName, artifact).String()]; excluded { + return label.NoLabel, nil + } + + return LabelFromArtifact(mavenRepositoryName, artifact), nil +} + func LabelFromArtifact(mavenRepositoryName string, artifact string) label.Label { return label.New(mavenRepositoryName, "", bazel.CleanupLabel(artifact)) } diff --git a/java/gazelle/private/types/types.go b/java/gazelle/private/types/types.go index 5b00ff31..6760e38a 100644 --- a/java/gazelle/private/types/types.go +++ b/java/gazelle/private/types/types.go @@ -112,6 +112,7 @@ type ResolveInput struct { ImportedPackageNames *sorted_set.SortedSet[PackageName] ImportedClasses *sorted_set.SortedSet[ClassName] ExportedPackageNames *sorted_set.SortedSet[PackageName] + ExportedClassNames *sorted_set.SortedSet[ClassName] AnnotationProcessors *sorted_set.SortedSet[ClassName] } diff --git a/java/gazelle/resolve.go b/java/gazelle/resolve.go index 0fece398..51ccf2a9 100644 --- a/java/gazelle/resolve.go +++ b/java/gazelle/resolve.go @@ -63,6 +63,22 @@ func (jr *Resolver) Imports(c *config.Config, r *rule.Rule, f *rule.File) []reso out = append(out, resolve.ImportSpec{Lang: languageName, Imp: pkg.String()}) } } + if classes := r.PrivateAttr(classesKey); classes != nil { + isTestRule := false + if literalExpr, ok := r.Attr("testonly").(*build.LiteralExpr); ok { + if literalExpr.Token == "True" { + isTestRule = true + } + } + + for _, class := range classes.([]types.ClassName) { + imp := class.FullyQualifiedClassName() + if isTestRule { + imp += "!testonly" + } + out = append(out, resolve.ImportSpec{Lang: languageName, Imp: imp}) + } + } log.Debug().Str("out", fmt.Sprintf("%#v", out)).Str("label", lbl.String()).Msg("return") return out @@ -123,8 +139,29 @@ func (jr *Resolver) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.Re func (jr *Resolver) populateAttr(c *config.Config, pc *javaconfig.Config, r *rule.Rule, attrName string, requiredPackageNames *sorted_set.SortedSet[types.PackageName], importedClasses *sorted_set.SortedSet[types.ClassName], ix *resolve.RuleIndex, isTestRule bool, from label.Label, ownPackageNames *sorted_set.SortedSet[types.PackageName]) { labels := sorted_set.NewSortedSetFn[label.Label]([]label.Label{}, sorted_set.LabelLess) + resolvedPackages := make(map[types.PackageName]bool) + + if importedClasses != nil { + for _, className := range importedClasses.SortedSlice() { + l, err := jr.lang.mavenResolver.ResolveClass(className, pc.ExcludedArtifacts(), pc.MavenRepositoryName()) + if err != nil { + jr.lang.logger.Warn().Err(err).Str("class", className.FullyQualifiedClassName()).Msg("error resolving class") + continue + } + if l == label.NoLabel { + l = jr.resolveSingleClass(c, pc, className, ix, from, isTestRule) + } + if l != label.NoLabel { + labels.Add(simplifyLabel(c.RepoName, l, from)) + resolvedPackages[className.PackageName()] = true + } + } + } for _, imp := range requiredPackageNames.SortedSlice() { + if resolvedPackages[imp] { + continue + } var pkgClasses []string if importedClasses != nil { for _, cls := range importedClasses.SortedSlice() { @@ -341,6 +378,57 @@ func (jr *Resolver) resolveSinglePackage(c *config.Config, pc *javaconfig.Config return label.NoLabel } +func (jr *Resolver) resolveSingleClass(c *config.Config, pc *javaconfig.Config, className types.ClassName, ix *resolve.RuleIndex, from label.Label, isTestRule bool) (out label.Label) { + imp := className.FullyQualifiedClassName() + importSpec := resolve.ImportSpec{Lang: languageName, Imp: imp} + if ol, found := resolve.FindRuleWithOverride(c, importSpec, languageName); found { + return ol + } + + matches := ix.FindRulesByImportWithConfig(c, importSpec, languageName) + + if pc.ResolveToJavaExports() { + matches = jr.tryResolvingToJavaExport(matches, from) + } else { + nonExportMatches := make([]resolve.FindResult, 0) + for _, match := range matches { + if !jr.lang.javaExportIndex.IsJavaExport(match.Label) { + nonExportMatches = append(nonExportMatches, match) + } + } + matches = nonExportMatches + } + + if len(matches) == 1 { + return matches[0].Label + } + + if len(matches) > 1 { + labels := make([]string, 0, len(matches)) + for _, match := range matches { + labels = append(labels, match.Label.String()) + } + sort.Strings(labels) + + jr.lang.logger.Error(). + Str("class", imp). + Strs("targets", labels). + Msg("resolveSingleClass found MULTIPLE results in rule index") + return label.NoLabel + } + + if isTestRule { + testImp := imp + "!testonly" + testSpec := resolve.ImportSpec{Lang: languageName, Imp: testImp} + testMatches := ix.FindRulesByImportWithConfig(c, testSpec, languageName) + if len(testMatches) == 1 { + return simplifyLabel(c.RepoName, testMatches[0].Label, from) + } + } + + return label.NoLabel +} + // tryResolvingToJavaExport attempts to narrow down a list of resolution candidates by preferring java_export targets when appropriate. // A dependency will be resolved to a `java_export` target when the following are all true. // - The dependency is contained in a java_export target, and diff --git a/java/gazelle/resolve_test.go b/java/gazelle/resolve_test.go index a8506491..36b7f7d5 100644 --- a/java/gazelle/resolve_test.go +++ b/java/gazelle/resolve_test.go @@ -381,6 +381,10 @@ func (*testResolver) Resolve(pkg types.PackageName, excludedArtifacts map[string return label.NoLabel, errors.New("not implemented") } +func (*testResolver) ResolveClass(className types.ClassName, excludedArtifacts map[string]struct{}, mavenRepositoryName string) (label.Label, error) { + return label.NoLabel, errors.New("not implemented") +} + type mapResolver map[string]resolve.Resolver func (mr mapResolver) Resolver(r *rule.Rule, f string) resolve.Resolver { @@ -425,3 +429,7 @@ func (r *TestMavenResolver) Resolve(pkg types.PackageName, excludedArtifacts map } return l, nil } + +func (r *TestMavenResolver) ResolveClass(className types.ClassName, excludedArtifacts map[string]struct{}, mavenRepositoryName string) (label.Label, error) { + return label.NoLabel, nil +} diff --git a/java/gazelle/testdata/kt_split_package/BUILD.in b/java/gazelle/testdata/kt_split_package/BUILD.in new file mode 100644 index 00000000..feccf582 --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/BUILD.in @@ -0,0 +1 @@ +# gazelle:jvm_kotlin_enabled true diff --git a/java/gazelle/testdata/kt_split_package/WORKSPACE b/java/gazelle/testdata/kt_split_package/WORKSPACE new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/WORKSPACE @@ -0,0 +1 @@ + diff --git a/java/gazelle/testdata/kt_split_package/app/src/main/BUILD.in b/java/gazelle/testdata/kt_split_package/app/src/main/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/kt_split_package/app/src/main/BUILD.out b/java/gazelle/testdata/kt_split_package/app/src/main/BUILD.out new file mode 100644 index 00000000..a60fc062 --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/app/src/main/BUILD.out @@ -0,0 +1,11 @@ +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "main", + srcs = ["Main.kt"], + visibility = ["//:__subpackages__"], + deps = [ + "//one/src", + "//two/src", + ], +) diff --git a/java/gazelle/testdata/kt_split_package/app/src/main/Main.kt b/java/gazelle/testdata/kt_split_package/app/src/main/Main.kt new file mode 100644 index 00000000..fd9fceb2 --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/app/src/main/Main.kt @@ -0,0 +1,9 @@ +package com.example.app + +import com.example.split.ClassA +import com.example.split.ClassB + +class Main { + val a = ClassA() + val b = ClassB() +} diff --git a/java/gazelle/testdata/kt_split_package/maven_install.json b/java/gazelle/testdata/kt_split_package/maven_install.json new file mode 100644 index 00000000..6c01c298 --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/maven_install.json @@ -0,0 +1 @@ +{"version": "2"} diff --git a/java/gazelle/testdata/kt_split_package/one/src/BUILD.in b/java/gazelle/testdata/kt_split_package/one/src/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/kt_split_package/one/src/BUILD.out b/java/gazelle/testdata/kt_split_package/one/src/BUILD.out new file mode 100644 index 00000000..299bf59c --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/one/src/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "src", + srcs = ["ClassA.kt"], + visibility = ["//:__subpackages__"], +) diff --git a/java/gazelle/testdata/kt_split_package/one/src/ClassA.kt b/java/gazelle/testdata/kt_split_package/one/src/ClassA.kt new file mode 100644 index 00000000..9dca3b1d --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/one/src/ClassA.kt @@ -0,0 +1,3 @@ +package com.example.split + +class ClassA diff --git a/java/gazelle/testdata/kt_split_package/two/src/BUILD.in b/java/gazelle/testdata/kt_split_package/two/src/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/kt_split_package/two/src/BUILD.out b/java/gazelle/testdata/kt_split_package/two/src/BUILD.out new file mode 100644 index 00000000..7148b475 --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/two/src/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "src", + srcs = ["ClassB.kt"], + visibility = ["//:__subpackages__"], +) diff --git a/java/gazelle/testdata/kt_split_package/two/src/ClassB.kt b/java/gazelle/testdata/kt_split_package/two/src/ClassB.kt new file mode 100644 index 00000000..b4c8520b --- /dev/null +++ b/java/gazelle/testdata/kt_split_package/two/src/ClassB.kt @@ -0,0 +1,3 @@ +package com.example.split + +class ClassB diff --git a/java/gazelle/testdata/split_package/BUILD.in b/java/gazelle/testdata/split_package/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/split_package/WORKSPACE b/java/gazelle/testdata/split_package/WORKSPACE new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/java/gazelle/testdata/split_package/WORKSPACE @@ -0,0 +1 @@ + diff --git a/java/gazelle/testdata/split_package/app/src/java/BUILD.in b/java/gazelle/testdata/split_package/app/src/java/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/split_package/app/src/java/BUILD.out b/java/gazelle/testdata/split_package/app/src/java/BUILD.out new file mode 100644 index 00000000..6c58a59d --- /dev/null +++ b/java/gazelle/testdata/split_package/app/src/java/BUILD.out @@ -0,0 +1,11 @@ +load("@rules_java//java:defs.bzl", "java_library") + +java_library( + name = "java", + srcs = ["Main.java"], + visibility = ["//:__subpackages__"], + deps = [ + "//one/src/java", + "//two/src/java", + ], +) diff --git a/java/gazelle/testdata/split_package/app/src/java/Main.java b/java/gazelle/testdata/split_package/app/src/java/Main.java new file mode 100644 index 00000000..f519e8e1 --- /dev/null +++ b/java/gazelle/testdata/split_package/app/src/java/Main.java @@ -0,0 +1,9 @@ +package com.example.app; + +import com.example.split.ClassA; +import com.example.split.ClassB; + +public class Main { + ClassA a; + ClassB b; +} diff --git a/java/gazelle/testdata/split_package/maven_install.json b/java/gazelle/testdata/split_package/maven_install.json new file mode 100644 index 00000000..6c01c298 --- /dev/null +++ b/java/gazelle/testdata/split_package/maven_install.json @@ -0,0 +1 @@ +{"version": "2"} diff --git a/java/gazelle/testdata/split_package/one/src/java/BUILD.in b/java/gazelle/testdata/split_package/one/src/java/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/split_package/one/src/java/BUILD.out b/java/gazelle/testdata/split_package/one/src/java/BUILD.out new file mode 100644 index 00000000..99a3c436 --- /dev/null +++ b/java/gazelle/testdata/split_package/one/src/java/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_java//java:defs.bzl", "java_library") + +java_library( + name = "java", + srcs = ["ClassA.java"], + visibility = ["//:__subpackages__"], +) diff --git a/java/gazelle/testdata/split_package/one/src/java/ClassA.java b/java/gazelle/testdata/split_package/one/src/java/ClassA.java new file mode 100644 index 00000000..1b7cf354 --- /dev/null +++ b/java/gazelle/testdata/split_package/one/src/java/ClassA.java @@ -0,0 +1,3 @@ +package com.example.split; + +public class ClassA {} diff --git a/java/gazelle/testdata/split_package/two/src/java/BUILD.in b/java/gazelle/testdata/split_package/two/src/java/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/split_package/two/src/java/BUILD.out b/java/gazelle/testdata/split_package/two/src/java/BUILD.out new file mode 100644 index 00000000..d697c663 --- /dev/null +++ b/java/gazelle/testdata/split_package/two/src/java/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_java//java:defs.bzl", "java_library") + +java_library( + name = "java", + srcs = ["ClassB.java"], + visibility = ["//:__subpackages__"], +) diff --git a/java/gazelle/testdata/split_package/two/src/java/ClassB.java b/java/gazelle/testdata/split_package/two/src/java/ClassB.java new file mode 100644 index 00000000..781228a2 --- /dev/null +++ b/java/gazelle/testdata/split_package/two/src/java/ClassB.java @@ -0,0 +1,3 @@ +package com.example.split; + +public class ClassB {} diff --git a/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/ClasspathParser.java b/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/ClasspathParser.java index 7b29c836..d359b404 100644 --- a/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/ClasspathParser.java +++ b/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/ClasspathParser.java @@ -14,6 +14,7 @@ import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.ImportTree; +import com.sun.source.tree.InstanceOfTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; @@ -22,6 +23,8 @@ import com.sun.source.tree.ParameterizedTypeTree; import com.sun.source.tree.PrimitiveTypeTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.TypeParameterTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.JavacTask; import com.sun.source.util.TreeScanner; @@ -195,6 +198,10 @@ public Void visitImport(ImportTree i, Void v) { @Override public Void visitClass(ClassTree t, Void v) { stack.addLast(t); + checkFullyQualifiedType(t.getExtendsClause()); + for (Tree implement : t.getImplementsClause()) { + checkFullyQualifiedType(implement); + } for (AnnotationTree annotation : t.getModifiers().getAnnotations()) { String annotationClassName = annotation.getAnnotationType().toString(); String importedFullyQualified = currentFileImports.get(annotationClassName); @@ -230,6 +237,10 @@ public Void visitMethod(com.sun.source.tree.MethodTree m, Void v) { } } + for (ExpressionTree thrown : m.getThrows()) { + checkFullyQualifiedType(thrown); + } + handleAnnotations(m.getModifiers().getAnnotations()); // Check to see if we have a main method @@ -337,6 +348,9 @@ public Void visitVariable(VariableTree node, Void unused) { @Nullable private Set checkFullyQualifiedType(Tree identifier) { + if (identifier == null) { + return null; + } Set types = new TreeSet<>(); if (identifier.getKind() == Tree.Kind.IDENTIFIER || identifier.getKind() == Tree.Kind.MEMBER_SELECT) { From 071a9ded141bd4a7add7e1f4020e0cf5a50439a1 Mon Sep 17 00:00:00 2001 From: Simon Mavi Stewart Date: Fri, 28 Nov 2025 16:30:31 +0000 Subject: [PATCH 2/3] Run formatter --- java/gazelle/generate.go | 4 ++-- java/gazelle/testdata/kt_split_package/WORKSPACE | 1 - java/gazelle/testdata/split_package/WORKSPACE | 1 - .../javaparser/generators/ClasspathParser.java | 3 --- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/java/gazelle/generate.go b/java/gazelle/generate.go index 796dc756..f3887efd 100644 --- a/java/gazelle/generate.go +++ b/java/gazelle/generate.go @@ -187,7 +187,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes allPackageNames.Add(mJavaPkg.Name) if !mJavaPkg.TestPackage { - addNonLocalImportsAndExports(productionJavaImports, productionJavaImportedClasses, nonLocalJavaExports, nonLocalJavaExportedClasses, mJavaPkg.ImportedClasses, mJavaPkg.ImportedPackagesWithoutSpecificClasses, mJavaPkg.ExportedClasses, mJavaPkg.Name, likelyLocalClassNames) + addNonLocalImportsAndExports(productionJavaImports, productionJavaImportedClasses, nonLocalJavaExports, nil, mJavaPkg.ImportedClasses, mJavaPkg.ImportedPackagesWithoutSpecificClasses, mJavaPkg.ExportedClasses, mJavaPkg.Name, likelyLocalClassNames) for _, f := range mJavaPkg.Files.SortedSlice() { productionJavaFiles.Add(filepath.Join(mRel, f)) jf := javaFile{pathRelativeToBazelWorkspaceRoot: filepath.Join(mRel, f), pkg: mJavaPkg.Name} @@ -216,7 +216,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes // Tests don't get to export things, as things shouldn't depend on them. addNonLocalImportsAndExports(testJavaImports, testJavaImportedClasses, nil, nil, javaPkg.ImportedClasses, javaPkg.ImportedPackagesWithoutSpecificClasses, javaPkg.ExportedClasses, javaPkg.Name, likelyLocalClassNames) } else { - addNonLocalImportsAndExports(productionJavaImports, productionJavaImportedClasses, nonLocalJavaExports, nonLocalJavaExportedClasses, javaPkg.ImportedClasses, javaPkg.ImportedPackagesWithoutSpecificClasses, javaPkg.ExportedClasses, javaPkg.Name, likelyLocalClassNames) + addNonLocalImportsAndExports(productionJavaImports, productionJavaImportedClasses, nonLocalJavaExports, nil, javaPkg.ImportedClasses, javaPkg.ImportedPackagesWithoutSpecificClasses, javaPkg.ExportedClasses, javaPkg.Name, likelyLocalClassNames) } allMains.AddAll(javaPkg.Mains) for _, f := range srcFilenamesRelativeToPackage { diff --git a/java/gazelle/testdata/kt_split_package/WORKSPACE b/java/gazelle/testdata/kt_split_package/WORKSPACE index 8b137891..e69de29b 100644 --- a/java/gazelle/testdata/kt_split_package/WORKSPACE +++ b/java/gazelle/testdata/kt_split_package/WORKSPACE @@ -1 +0,0 @@ - diff --git a/java/gazelle/testdata/split_package/WORKSPACE b/java/gazelle/testdata/split_package/WORKSPACE index 8b137891..e69de29b 100644 --- a/java/gazelle/testdata/split_package/WORKSPACE +++ b/java/gazelle/testdata/split_package/WORKSPACE @@ -1 +0,0 @@ - diff --git a/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/ClasspathParser.java b/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/ClasspathParser.java index d359b404..421f587d 100644 --- a/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/ClasspathParser.java +++ b/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/ClasspathParser.java @@ -14,7 +14,6 @@ import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.ImportTree; -import com.sun.source.tree.InstanceOfTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; @@ -23,8 +22,6 @@ import com.sun.source.tree.ParameterizedTypeTree; import com.sun.source.tree.PrimitiveTypeTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.TypeCastTree; -import com.sun.source.tree.TypeParameterTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.JavacTask; import com.sun.source.util.TreeScanner; From 42c558a2b295ee8996be2be937dff2cccfc5040c Mon Sep 17 00:00:00 2001 From: Simon Mavi Stewart Date: Mon, 1 Dec 2025 15:00:59 +0000 Subject: [PATCH 3/3] fix: sort java_test_suite srcs to ensure deterministic output on Windows --- java/gazelle/generate.go | 1 + 1 file changed, 1 insertion(+) diff --git a/java/gazelle/generate.go b/java/gazelle/generate.go index f3887efd..ac5d5e6f 100644 --- a/java/gazelle/generate.go +++ b/java/gazelle/generate.go @@ -403,6 +403,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes srcs = append(srcs, strings.TrimPrefix(filepath.ToSlash(src.pathRelativeToBazelWorkspaceRoot), args.Rel+"/")) } } + sort.Strings(srcs) if len(srcs) > 0 { l.generateJavaTestSuite( args.File,