Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 46 additions & 12 deletions java/gazelle/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,17 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes
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}
nonLocalJavaExportedClasses.Add(*jf.ClassName())
}
// Add all defined classes (including inner classes) for class-level resolution.
// Use DefinedClasses if available (Java files), otherwise fall back to
// deriving class names from filenames (Kotlin files).
if mJavaPkg.DefinedClasses != nil && mJavaPkg.DefinedClasses.Len() > 0 {
nonLocalJavaExportedClasses.AddAll(mJavaPkg.DefinedClasses)
} else {
for _, f := range mJavaPkg.Files.SortedSlice() {
jf := javaFile{pathRelativeToBazelWorkspaceRoot: filepath.Join(mRel, f), pkg: mJavaPkg.Name}
nonLocalJavaExportedClasses.Add(*jf.ClassName())
}
}
allMains.AddAll(mJavaPkg.Mains)
} else {
Expand Down Expand Up @@ -237,8 +246,20 @@ 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())
}
}
// Add all defined classes (including inner classes) for class-level resolution.
// Use DefinedClasses if available (Java files), otherwise fall back to
// deriving class names from filenames (Kotlin files).
if !javaPkg.TestPackage {
if javaPkg.DefinedClasses != nil && javaPkg.DefinedClasses.Len() > 0 {
nonLocalJavaExportedClasses.AddAll(javaPkg.DefinedClasses)
} else {
for _, f := range srcFilenamesRelativeToPackage {
path := filepath.Join(args.Rel, f)
jf := javaFile{pathRelativeToBazelWorkspaceRoot: path, pkg: javaPkg.Name}
nonLocalJavaExportedClasses.Add(*jf.ClassName())
}
}
}
for _, annotationClass := range javaPkg.AllAnnotations().SortedSlice() {
Expand All @@ -247,17 +268,30 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes
}

allPackageNamesSlice := allPackageNames.SortedSlice()
// For imported classes, we can't just filter by package name because of split packages.
// A class might be in the same Java package as local classes but defined in a different
// Bazel package. The addNonLocalImportsAndExports function already correctly filtered out
// locally-defined classes, so we just need to filter out classes with empty package names.
nonLocalProductionJavaImportedClasses := productionJavaImportedClasses.Filter(func(c types.ClassName) bool {
return c.PackageName().Name != ""
})

// Build a set of packages that have non-local imported classes.
// This is needed for split-package scenarios where a class is in the same Java package
// but a different Bazel package.
packagesWithNonLocalClasses := sorted_set.NewSortedSetFn([]types.PackageName{}, types.PackageNameLess)
for _, cls := range nonLocalProductionJavaImportedClasses.SortedSlice() {
packagesWithNonLocalClasses.Add(cls.PackageName())
}

nonLocalProductionJavaImports := productionJavaImports.Filter(func(i types.PackageName) bool {
for _, n := range allPackageNamesSlice {
if i.Name == n.Name {
return false
}
// Keep packages that have non-local imported classes
if packagesWithNonLocalClasses.Contains(i) {
return true
}
return true
})
nonLocalProductionJavaImportedClasses := productionJavaImportedClasses.Filter(func(c types.ClassName) bool {
// Filter out packages that match the local packages
for _, n := range allPackageNamesSlice {
if c.PackageName().Name == n.Name {
if i.Name == n.Name {
return false
}
}
Expand Down
5 changes: 5 additions & 0 deletions java/gazelle/private/java/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ type Package struct {
ImportedPackagesWithoutSpecificClasses *sorted_set.SortedSet[types.PackageName]
Mains *sorted_set.SortedSet[types.ClassName]

// DefinedClasses contains all public classes defined in this package, including inner classes.
// This is used for class-level resolution in split-package scenarios where inner classes
// can be referenced by simple name from the same Java package.
DefinedClasses *sorted_set.SortedSet[types.ClassName]

// Especially useful for module mode
Files *sorted_set.SortedSet[string]
TestPackage bool
Expand Down
18 changes: 18 additions & 0 deletions java/gazelle/private/javaparser/javaparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ func (r Runner) ParsePackage(ctx context.Context, in *ParsePackageRequest) (*jav
}
importedClasses.Add(*className)
}
// Add same-package type references as fully qualified class names.
// These are simple type names (e.g., "Clock") that Java doesn't require imports for
// because they're in the same package as the referencing class.
for _, simpleTypeName := range resp.GetSamePackageTypeReferences() {
if packageName.Name != "" {
className := types.NewClassName(packageName, simpleTypeName)
importedClasses.Add(className)
}
}
exportedClasses := sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess)
for _, export := range resp.GetExportedClasses() {
className, err := types.ParseClassName(export)
Expand All @@ -130,13 +139,22 @@ func (r Runner) ParsePackage(ctx context.Context, in *ParsePackageRequest) (*jav
for _, main := range resp.GetMains() {
mains.Add(types.NewClassName(packageName, main))
}
definedClasses := sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess)
for _, defined := range resp.GetDefinedClasses() {
className, err := types.ParseClassName(defined)
if err != nil {
return nil, fmt.Errorf("failed to parse defined class %q: %w", defined, err)
}
definedClasses.Add(*className)
}

return &java.Package{
Name: packageName,
ImportedClasses: importedClasses,
ExportedClasses: exportedClasses,
ImportedPackagesWithoutSpecificClasses: importedPackages,
Mains: mains,
DefinedClasses: definedClasses,
Files: sorted_set.NewSortedSet(in.Files),
TestPackage: java.IsTestPackage(in.Rel),
PerClassMetadata: perClassMetadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ message Package {

// The list of classes which dependers may need to have exported to them because they are part of the public interface of the request.
repeated string exported_classes = 6;

// Simple type names (without package prefix) that are referenced in type contexts but not
// explicitly imported. These are likely same-package type references that Java doesn't require
// imports for. The Go side should combine these with the package name to form fully qualified
// class names.
repeated string same_package_type_references = 7;

// Fully qualified names of all public classes defined in this package, including inner classes.
// For example, for a file containing `public class Outer { public static class Inner {} }`,
// this would contain both "com.example.Outer" and "com.example.Outer.Inner".
// This is used for class-level resolution in split-package scenarios.
repeated string defined_classes = 8;
}

message PerClassMetadata {
Expand Down
10 changes: 10 additions & 0 deletions java/gazelle/private/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ func (c *ClassName) BareOuterClassName() string {
return c.bareOuterClassName
}

// BareInnermostClassName returns the simple name of the innermost class.
// For `com.example.Outer.Inner.Deep`, this returns "Deep".
// For `com.example.Simple`, this returns "Simple".
func (c *ClassName) BareInnermostClassName() string {
if len(c.innerClassNames) > 0 {
return c.innerClassNames[len(c.innerClassNames)-1]
}
return c.bareOuterClassName
}

func (c *ClassName) FullyQualifiedOuterClassName() string {
var parts []string
if c.packageName.Name != "" {
Expand Down
76 changes: 56 additions & 20 deletions java/gazelle/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ type packageClassIndex struct {
test map[string][]label.Label
}

// addUnique adds a label to the slice for a given key, but only if it's not already present.
func (pci *packageClassIndex) addProd(key string, l label.Label) {
for _, existing := range pci.prod[key] {
if existing == l {
return
}
}
pci.prod[key] = append(pci.prod[key], l)
}

func (pci *packageClassIndex) addTest(key string, l label.Label) {
for _, existing := range pci.test[key] {
if existing == l {
return
}
}
pci.test[key] = append(pci.test[key], l)
}

func NewResolver(lang *javaLang) *Resolver {
internalCache, err := lru.New(10000)
if err != nil {
Expand Down Expand Up @@ -157,7 +176,7 @@ func (jr *Resolver) populateAttr(c *config.Config, pc *javaconfig.Config, r *rul
}

// Try package-level resolution first (fast path)
dep, ambiguous := jr.resolveSinglePackageWithAmbiguity(c, pc, imp, ix, from, isTestRule, ownPackageNames, pkgClasses)
dep, ambiguous, providers := jr.resolveSinglePackageWithAmbiguity(c, pc, imp, ix, from, isTestRule, ownPackageNames, pkgClasses)
if dep != label.NoLabel {
labels.Add(simplifyLabel(c.RepoName, dep, from))
continue
Expand All @@ -168,6 +187,7 @@ func (jr *Resolver) populateAttr(c *config.Config, pc *javaconfig.Config, r *rul
jr.lang.logger.Debug().
Str("package", imp.Name).
Strs("classes", pkgClasses).
Strs("providers", providers).
Stringer("from", from).
Msg("package has multiple providers, attempting class-level resolution")

Expand All @@ -191,6 +211,7 @@ func (jr *Resolver) populateAttr(c *config.Config, pc *javaconfig.Config, r *rul
jr.lang.logger.Error().
Str("package", imp.Name).
Strs("classes", pkgClasses).
Strs("providers", providers).
Stringer("from", from).
Msg("package has multiple providers and class-level resolution failed for all classes")
jr.lang.hasHadErrors = true
Expand Down Expand Up @@ -273,11 +294,11 @@ func setLabelAttrIncludingExistingValues(r *rule.Rule, attrName string, labels *

// resolveSinglePackageWithAmbiguity resolves a package import and returns whether there was ambiguity.
// When ambiguous is true and out is NoLabel, the caller should attempt class-level resolution.
func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *javaconfig.Config, imp types.PackageName, ix *resolve.RuleIndex, from label.Label, isTestRule bool, ownPackageNames *sorted_set.SortedSet[types.PackageName], pkgClasses []string) (out label.Label, ambiguous bool) {
func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *javaconfig.Config, imp types.PackageName, ix *resolve.RuleIndex, from label.Label, isTestRule bool, ownPackageNames *sorted_set.SortedSet[types.PackageName], pkgClasses []string) (out label.Label, ambiguous bool, providers []string) {
cacheKey := types.NewResolvableJavaPackage(imp, false, false)
importSpec := resolve.ImportSpec{Lang: languageName, Imp: cacheKey.String()}
if ol, found := resolve.FindRuleWithOverride(c, importSpec, languageName); found {
return ol, false
return ol, false, nil
}

matches := ix.FindRulesByImportWithConfig(c, importSpec, languageName)
Expand All @@ -295,16 +316,20 @@ func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *java
}

if len(matches) == 1 {
return matches[0].Label, false
return matches[0].Label, false, nil
}

if len(matches) > 1 {
// Multiple matches found - signal ambiguity so caller can try class-level resolution
return label.NoLabel, true
providerLabels := make([]string, len(matches))
for i, m := range matches {
providerLabels[i] = m.Label.String()
}
return label.NoLabel, true, providerLabels
}

if v, ok := jr.internalCache.Get(cacheKey); ok {
return simplifyLabel(c.RepoName, v.(label.Label), from), false
return simplifyLabel(c.RepoName, v.(label.Label), from), false, nil
}

jr.lang.logger.Debug().Str("parsedImport", imp.Name).Stringer("from", from).Msg("not found yet")
Expand All @@ -316,10 +341,10 @@ func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *java
}()

if java.IsStdlib(imp) {
return label.NoLabel, false
return label.NoLabel, false, nil
}
if kotlin.IsStdlib(imp) {
return label.NoLabel, false
return label.NoLabel, false, nil
}

// As per https://github.com/bazelbuild/bazel/blob/347407a88fd480fc5e0fbd42cc8196e4356a690b/tools/java/runfiles/Runfiles.java#L41
Expand All @@ -328,9 +353,9 @@ func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *java
l, err := label.Parse(runfilesLabel)
if err != nil {
jr.lang.logger.Fatal().Str("label", runfilesLabel).Err(err).Msg("failed to parse known-good runfiles label")
return label.NoLabel, false
return label.NoLabel, false, nil
}
return l, false
return l, false, nil
}

if l, err := jr.lang.mavenResolver.Resolve(imp, pc.ExcludedArtifacts(), pc.MavenRepositoryName()); err != nil {
Expand All @@ -346,7 +371,7 @@ func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *java
for _, className := range pkgClasses {
cls := types.NewClassName(imp, className)
if resolved, _ := jr.lang.mavenResolver.ResolveClass(cls, pc.ExcludedArtifacts(), pc.MavenRepositoryName()); resolved != label.NoLabel {
return label.NoLabel, true
return label.NoLabel, true, multipleExternal.PossiblePackages
}
}
}
Expand All @@ -360,7 +385,7 @@ func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *java
jr.lang.logger.Fatal().Err(err).Msg("maven resolver error")
}
} else {
return l, false
return l, false, nil
}

if isTestRule {
Expand All @@ -370,7 +395,7 @@ func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *java
testonlyMatches := ix.FindRulesByImportWithConfig(c, testonlyImportSpec, languageName)
if len(testonlyMatches) == 1 {
cacheKey = testonlyCacheKey
return simplifyLabel(c.RepoName, testonlyMatches[0].Label, from), false
return simplifyLabel(c.RepoName, testonlyMatches[0].Label, from), false, nil
}

// If there's exactly one testsuite match, use it
Expand All @@ -382,14 +407,14 @@ func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *java
l := testsuiteMatches[0].Label
if l != from {
l.Name += "-test-lib"
return simplifyLabel(c.RepoName, l, from), false
return simplifyLabel(c.RepoName, l, from), false, nil
}
}
}

if isTestRule && ownPackageNames.Contains(imp) {
// Tests may have unique packages which don't exist outside of those tests - don't treat this as an error.
return label.NoLabel, false
return label.NoLabel, false, nil
}

jr.lang.logger.Warn().
Expand All @@ -399,11 +424,11 @@ func (jr *Resolver) resolveSinglePackageWithAmbiguity(c *config.Config, pc *java
Msg("Unable to find package for import in any dependency")
jr.lang.hasHadErrors = true

return label.NoLabel, false
return label.NoLabel, false, nil
}

func (jr *Resolver) resolveSinglePackage(c *config.Config, pc *javaconfig.Config, imp types.PackageName, ix *resolve.RuleIndex, from label.Label, isTestRule bool, ownPackageNames *sorted_set.SortedSet[types.PackageName], pkgClasses []string) (out label.Label) {
out, _ = jr.resolveSinglePackageWithAmbiguity(c, pc, imp, ix, from, isTestRule, ownPackageNames, pkgClasses)
out, _, _ = jr.resolveSinglePackageWithAmbiguity(c, pc, imp, ix, from, isTestRule, ownPackageNames, pkgClasses)
return out
}

Expand Down Expand Up @@ -441,11 +466,22 @@ func (jr *Resolver) buildPackageClassIndex(c *config.Config, pkg types.PackageNa
if cls.PackageName() != pkg {
continue
}
name := cls.BareOuterClassName()
// Index by outer class name
outerName := cls.BareOuterClassName()
if info.testonly {
pci.test[name] = append(pci.test[name], m.Label)
pci.addTest(outerName, m.Label)
} else {
pci.prod[name] = append(pci.prod[name], m.Label)
pci.addProd(outerName, m.Label)
}
// Also index by innermost class name for same-package inner class references.
// In Java, inner classes can be referenced by simple name from within the same package.
innerName := cls.BareInnermostClassName()
if innerName != outerName {
if info.testonly {
pci.addTest(innerName, m.Label)
} else {
pci.addProd(innerName, m.Label)
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions java/gazelle/testdata/split_package_private_class/BUILD.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

30 changes: 30 additions & 0 deletions java/gazelle/testdata/split_package_private_class/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Split Package with Private Inner Class Test

This test verifies that Gazelle's Java extension correctly handles split packages
where a file:
1. Uses **private inner classes** defined within the same file
2. References classes from other files in the same package via `.class` literals

## Scenario

- `annotations/src/java/` contains `Uninterruptible.java` (an annotation) with package `com.example.uninterruptibles`
- `lib/src/java/` contains `UninterruptibleListener.java` with the same package, including:
- A reference to `Uninterruptible.class` (from the annotations package)
- Private inner classes like `InvokeWithExceptionHandling`
- `app/src/java/` imports `com.example.uninterruptibles.UninterruptibleListener`

## Bugs Fixed

### Bug 1: Private inner class references causing resolution failure
When a file used a private inner class from within the same file, and the
package had multiple providers (split package scenario), resolution would fail.

**Fix**: Track all class names (including private) per file and filter them out
from same-package references at the end of each file's processing.

### Bug 2: Class literals not detected as dependencies
Class literals like `Foo.class` were not being detected as type references,
so dependencies on same-package classes used via `.class` were missing.

**Fix**: Added `visitMemberSelect` to detect class literals and treat them
as same-package type references when the class name is unqualified.
Empty file.
Loading
Loading