C
This commit is contained in:
@@ -347,6 +347,10 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass 2: register inheritance under canonical FQN keys once all types are indexed.
|
||||||
|
// Single-pass writes in indexType are unsafe when a subclass is scanned before its superclass.
|
||||||
|
indexInheritanceEdges();
|
||||||
|
|
||||||
this.implementationsCache.clear();
|
this.implementationsCache.clear();
|
||||||
this.accessorIndex = inlineAccessors
|
this.accessorIndex = inlineAccessors
|
||||||
? new AccessorIndexBuilder().build(this)
|
? new AccessorIndexBuilder().build(this)
|
||||||
@@ -388,21 +392,6 @@ public class CodebaseContext {
|
|||||||
simpleNameToFqn.put(simpleName, fqn);
|
simpleNameToFqn.put(simpleName, fqn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inheritance Mapping
|
|
||||||
// Track implementations (for both interfaces and superclasses)
|
|
||||||
if (td.superInterfaceTypes() != null) {
|
|
||||||
for (Object itfObj : td.superInterfaceTypes()) {
|
|
||||||
Type itf = (Type) itfObj;
|
|
||||||
String itfName = extractTypeName(itf);
|
|
||||||
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Type superclass = td.getSuperclassType();
|
|
||||||
if (superclass != null) {
|
|
||||||
String superName = extractTypeName(superclass);
|
|
||||||
interfaceToImpls.computeIfAbsent(superName, k -> new ArrayList<>()).add(fqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively index nested types
|
// Recursively index nested types
|
||||||
for (Object decl : td.bodyDeclarations()) {
|
for (Object decl : td.bodyDeclarations()) {
|
||||||
if (decl instanceof TypeDeclaration nestedTd) {
|
if (decl instanceof TypeDeclaration nestedTd) {
|
||||||
@@ -415,6 +404,54 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After all types are indexed, register extends/implements under a canonical key:
|
||||||
|
* resolved FQN when the supertype is in the codebase, otherwise the literal AST name.
|
||||||
|
*/
|
||||||
|
private void indexInheritanceEdges() {
|
||||||
|
interfaceToImpls.clear();
|
||||||
|
// Snapshot keys: resolveSupertypeIndexKey -> getTypeDeclaration may cache into typeDeclarations.
|
||||||
|
for (String implFqn : new ArrayList<>(typeDeclarations.keySet())) {
|
||||||
|
TypeDeclaration td = typeDeclarations.get(implFqn);
|
||||||
|
if (td == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit root ? root : null;
|
||||||
|
|
||||||
|
if (td.superInterfaceTypes() != null) {
|
||||||
|
for (Object itfObj : td.superInterfaceTypes()) {
|
||||||
|
if (itfObj instanceof Type itf) {
|
||||||
|
String key = resolveSupertypeIndexKey(itf, cu);
|
||||||
|
interfaceToImpls.computeIfAbsent(key, k -> new ArrayList<>()).add(implFqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type superclass = td.getSuperclassType();
|
||||||
|
if (superclass != null) {
|
||||||
|
String key = resolveSupertypeIndexKey(superclass, cu);
|
||||||
|
interfaceToImpls.computeIfAbsent(key, k -> new ArrayList<>()).add(implFqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical index key for a supertype reference: FQN when resolvable in-context, else raw AST name.
|
||||||
|
*/
|
||||||
|
private String resolveSupertypeIndexKey(Type type, CompilationUnit cu) {
|
||||||
|
String raw = extractTypeName(type);
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
TypeDeclaration superTd = getTypeDeclaration(raw, cu);
|
||||||
|
if (superTd != null) {
|
||||||
|
String fqn = getFqn(superTd);
|
||||||
|
if (fqn != null && !fqn.isBlank()) {
|
||||||
|
return fqn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
private void indexRecord(org.eclipse.jdt.core.dom.RecordDeclaration rd, String parentFqn, Path javaFile) {
|
private void indexRecord(org.eclipse.jdt.core.dom.RecordDeclaration rd, String parentFqn, Path javaFile) {
|
||||||
String simpleName = rd.getName().getIdentifier();
|
String simpleName = rd.getName().getIdentifier();
|
||||||
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|||||||
@@ -301,6 +301,57 @@ class CodebaseContextTest {
|
|||||||
.containsExactlyInAnyOrder("abcd.ccc.Av", "abcd.ccc.RealListener");
|
.containsExactlyInAnyOrder("abcd.ccc.Av", "abcd.ccc.RealListener");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImplementationsShouldResolveWhenSubclassScannedBeforeSuperclass() throws IOException {
|
||||||
|
Files.createDirectories(tempDir.resolve("abcd/ccc"));
|
||||||
|
// Names sort before AML so Files.walk typically indexes subclasses first.
|
||||||
|
Files.writeString(tempDir.resolve("abcd/ccc/AaaListener.java"), """
|
||||||
|
package abcd.ccc;
|
||||||
|
public class AaaListener extends AML {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("abcd/ccc/Av.java"), """
|
||||||
|
package abcd.ccc;
|
||||||
|
public class Av extends abcd.ccc.AML {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("abcd/ccc/AML.java"), """
|
||||||
|
package abcd.ccc;
|
||||||
|
public abstract class AML {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
assertThat(context.getImplementations("abcd.ccc.AML"))
|
||||||
|
.containsExactlyInAnyOrder("abcd.ccc.AaaListener", "abcd.ccc.Av");
|
||||||
|
assertThat(context.getImplementations("AML"))
|
||||||
|
.containsExactlyInAnyOrder("abcd.ccc.AaaListener", "abcd.ccc.Av");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImplementationsShouldCanonicalizeImportAwareExtendsToSameFqnBucket() throws IOException {
|
||||||
|
Files.createDirectories(tempDir.resolve("abcd/ccc"));
|
||||||
|
Files.createDirectories(tempDir.resolve("other/pkg"));
|
||||||
|
Files.writeString(tempDir.resolve("abcd/ccc/AML.java"), """
|
||||||
|
package abcd.ccc;
|
||||||
|
public abstract class AML {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("abcd/ccc/SamePackageListener.java"), """
|
||||||
|
package abcd.ccc;
|
||||||
|
public class SamePackageListener extends AML {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("other/pkg/ImportedListener.java"), """
|
||||||
|
package other.pkg;
|
||||||
|
import abcd.ccc.AML;
|
||||||
|
public class ImportedListener extends AML {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
assertThat(context.getImplementations("abcd.ccc.AML"))
|
||||||
|
.containsExactlyInAnyOrder("abcd.ccc.SamePackageListener", "other.pkg.ImportedListener");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void areSameTypeOrUnambiguousSimpleMatchShouldFailClosedForAmbiguousTypes() throws IOException {
|
void areSameTypeOrUnambiguousSimpleMatchShouldFailClosedForAmbiguousTypes() throws IOException {
|
||||||
Files.createDirectories(tempDir.resolve("a"));
|
Files.createDirectories(tempDir.resolve("a"));
|
||||||
|
|||||||
Reference in New Issue
Block a user