This commit is contained in:
2026-07-17 08:48:09 +02:00
parent 18872e26a8
commit a463291f9a
2 changed files with 102 additions and 40 deletions

View File

@@ -476,46 +476,8 @@ public class CodebaseContext {
return;
}
// Try direct match
List<String> directImpls = interfaceToImpls.get(cleanName);
String directImplsSource = directImpls != null ? "direct" : null;
// Try FQN match if input was simple name
if (directImpls == null) {
String fqn = simpleNameToFqn.get(typeName);
if (fqn != null) {
directImpls = interfaceToImpls.get(fqn);
if (directImpls != null) {
directImplsSource = "simple→fqn";
}
}
}
// Try simple name match if input was FQN
if (directImpls == null && cleanName.contains(".")) {
String simpleName = cleanName.substring(cleanName.lastIndexOf('.') + 1);
List<String> simpleImpls = interfaceToImpls.get(simpleName);
if (simpleImpls != null) {
if (ambiguousSimpleNames.contains(simpleName)) {
String pkg = cleanName.substring(0, cleanName.lastIndexOf('.'));
List<String> pkgScoped = new ArrayList<>();
for (String impl : simpleImpls) {
if (impl.startsWith(pkg + ".")) {
pkgScoped.add(impl);
}
}
directImpls = pkgScoped.isEmpty() ? null : pkgScoped;
} else {
directImpls = simpleImpls;
}
if (directImpls != null) {
directImplsSource = "fqn→simple";
}
}
}
if (directImplsSource == null) {
directImplsSource = "none";
}
List<String> directImpls = resolveDirectImplementations(typeName, cleanName);
String directImplsSource = describeDirectImplsSource(typeName, cleanName);
if (log.isDebugEnabled()) {
log.debug(
@@ -535,6 +497,80 @@ public class CodebaseContext {
}
}
/**
* Union direct subclasses/implementors from every index bucket that refers to the same supertype.
* Generated stubs often index under a literal FQN ({@code extends abcd.ccc.AML}) while hand-written
* sources index under the simple name ({@code extends AML}) — merge instead of stopping at the first hit.
*/
private List<String> resolveDirectImplementations(String typeName, String cleanName) {
LinkedHashSet<String> merged = new LinkedHashSet<>();
if (cleanName == null || cleanName.isBlank()) {
return null;
}
addImplsForKey(merged, cleanName);
if (!cleanName.contains(".")) {
String fqn = simpleNameToFqn.get(typeName != null ? typeName : cleanName);
if (fqn != null && !fqn.equals(cleanName)) {
addImplsForKey(merged, fqn);
}
} else {
String simpleName = cleanName.substring(cleanName.lastIndexOf('.') + 1);
if (!ambiguousSimpleNames.contains(simpleName)) {
addImplsForKey(merged, simpleName);
} else {
String pkg = cleanName.substring(0, cleanName.lastIndexOf('.'));
List<String> simpleImpls = interfaceToImpls.get(simpleName);
if (simpleImpls != null) {
for (String impl : simpleImpls) {
if (impl.startsWith(pkg + ".")) {
merged.add(impl);
}
}
}
}
}
return merged.isEmpty() ? null : new ArrayList<>(merged);
}
private void addImplsForKey(Set<String> merged, String key) {
List<String> impls = interfaceToImpls.get(key);
if (impls != null) {
merged.addAll(impls);
}
}
private String describeDirectImplsSource(String typeName, String cleanName) {
if (cleanName == null || cleanName.isBlank()) {
return "none";
}
boolean hasDirect = interfaceToImpls.containsKey(cleanName);
boolean hasFqnAlias = false;
boolean hasSimpleAlias = false;
if (!cleanName.contains(".")) {
String fqn = simpleNameToFqn.get(typeName != null ? typeName : cleanName);
hasFqnAlias = fqn != null && interfaceToImpls.containsKey(fqn);
} else {
String simpleName = cleanName.substring(cleanName.lastIndexOf('.') + 1);
hasSimpleAlias = interfaceToImpls.containsKey(simpleName);
}
if (hasDirect && (hasFqnAlias || hasSimpleAlias)) {
return "merged";
}
if (hasDirect) {
return "direct";
}
if (hasFqnAlias) {
return "simple→fqn";
}
if (hasSimpleAlias) {
return "fqn→simple";
}
return "none";
}
private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) {
String simpleName = ed.getName().getIdentifier();
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;

View File

@@ -275,6 +275,32 @@ class CodebaseContextTest {
assertThat(context.getImplementations("b.Service")).containsExactly("b.BServiceImpl");
}
@Test
void getImplementationsShouldMergeFqnAndSimpleNameIndexBuckets() throws IOException {
Files.createDirectories(tempDir.resolve("abcd/ccc"));
Files.writeString(tempDir.resolve("abcd/ccc/AML.java"), """
package abcd.ccc;
public abstract class AML {}
""");
Files.writeString(tempDir.resolve("abcd/ccc/RealListener.java"), """
package abcd.ccc;
public class RealListener extends AML {}
""");
// KAPT-style stub: no import, FQN in extends clause
Files.writeString(tempDir.resolve("abcd/ccc/Av.java"), """
package abcd.ccc;
public class Av extends abcd.ccc.AML {}
""");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.getImplementations("abcd.ccc.AML"))
.containsExactlyInAnyOrder("abcd.ccc.Av", "abcd.ccc.RealListener");
assertThat(context.getImplementations("AML"))
.containsExactlyInAnyOrder("abcd.ccc.Av", "abcd.ccc.RealListener");
}
@Test
void areSameTypeOrUnambiguousSimpleMatchShouldFailClosedForAmbiguousTypes() throws IOException {
Files.createDirectories(tempDir.resolve("a"));