C
This commit is contained in:
@@ -1591,21 +1591,21 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||||
TypeDeclaration currentType = enclosingType;
|
final String[] resolvedType = {null};
|
||||||
while (currentType != null) {
|
context.forEachSuperclass(enclosingType, currentType -> {
|
||||||
|
if (resolvedType[0] != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (FieldDeclaration field : currentType.getFields()) {
|
for (FieldDeclaration field : currentType.getFields()) {
|
||||||
for (Object fragObj : field.fragments()) {
|
for (Object fragObj : field.fragments()) {
|
||||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
if (frag.getName().getIdentifier().equals(varName)) {
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
return resolveTypeToFqn(field.getType(), receiverNameNode);
|
resolvedType[0] = resolveTypeToFqn(field.getType(), receiverNameNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String superclass = context.getSuperclassFqn(currentType);
|
});
|
||||||
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
return resolvedType[0];
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
protected String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
||||||
@@ -1820,19 +1820,18 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
while (superFqn != null) {
|
TypeDeclaration superTd = superFqn != null ? context.getTypeDeclaration(superFqn) : null;
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
if (superTd == null) {
|
||||||
if (superTd != null) {
|
return null;
|
||||||
resolved = resolveMethodInType(superTd, methodName);
|
|
||||||
if (resolved != null) {
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
superFqn = context.getSuperclassFqn(superTd);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
final String[] found = {null};
|
||||||
|
context.forEachSuperclass(superTd, current -> {
|
||||||
|
if (found[0] != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
found[0] = resolveMethodInType(current, methodName);
|
||||||
|
});
|
||||||
|
return found[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Expression traceVariable(Expression expr) {
|
protected Expression traceVariable(Expression expr) {
|
||||||
|
|||||||
@@ -788,11 +788,18 @@ public final class EntryPointBindingExpander {
|
|||||||
TypeDeclaration typeDecl,
|
TypeDeclaration typeDecl,
|
||||||
String fieldName,
|
String fieldName,
|
||||||
CodebaseContext context) {
|
CodebaseContext context) {
|
||||||
for (TypeDeclaration current = typeDecl; current != null; current = superTypeDeclaration(current, context)) {
|
final Map<String, String>[] found = new Map[1];
|
||||||
|
context.forEachSuperclass(typeDecl, current -> {
|
||||||
|
if (found[0] != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Map<String, String> entries = extractStaticFieldMapInitializerOnType(current, fieldName, context);
|
Map<String, String> entries = extractStaticFieldMapInitializerOnType(current, fieldName, context);
|
||||||
if (entries != null) {
|
if (entries != null) {
|
||||||
return entries;
|
found[0] = entries;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (found[0] != null) {
|
||||||
|
return found[0];
|
||||||
}
|
}
|
||||||
return extractStaticFieldMapInitializerFromInterfaces(typeDecl, fieldName, context, new LinkedHashSet<>());
|
return extractStaticFieldMapInitializerFromInterfaces(typeDecl, fieldName, context, new LinkedHashSet<>());
|
||||||
}
|
}
|
||||||
@@ -835,14 +842,6 @@ public final class EntryPointBindingExpander {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TypeDeclaration superTypeDeclaration(TypeDeclaration typeDecl, CodebaseContext context) {
|
|
||||||
String superFqn = context.getSuperclassFqn(typeDecl);
|
|
||||||
if (superFqn == null || "java.lang.Object".equals(superFqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return context.getTypeDeclaration(superFqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extractStaticFieldMapInitializerOnType(
|
private static Map<String, String> extractStaticFieldMapInitializerOnType(
|
||||||
TypeDeclaration typeDecl,
|
TypeDeclaration typeDecl,
|
||||||
String fieldName,
|
String fieldName,
|
||||||
|
|||||||
@@ -280,13 +280,30 @@ public class TypeResolver {
|
|||||||
if (td != null) {
|
if (td != null) {
|
||||||
if (td instanceof TypeDeclaration typeDecl) {
|
if (td instanceof TypeDeclaration typeDecl) {
|
||||||
String superFqn = context.getSuperclassFqn(typeDecl);
|
String superFqn = context.getSuperclassFqn(typeDecl);
|
||||||
while (superFqn != null) {
|
TypeDeclaration superTd = superFqn != null ? context.getTypeDeclaration(superFqn) : null;
|
||||||
if (superFqn.contains("<")) {
|
if (superTd != null) {
|
||||||
superFqn = superFqn.substring(0, superFqn.indexOf('<'));
|
final String normalizedExpectedType = expectedType;
|
||||||
|
final boolean[] matched = {false};
|
||||||
|
context.forEachSuperclass(superTd, current -> {
|
||||||
|
if (matched[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String currentFqn = context.getFqn(current);
|
||||||
|
if (currentFqn == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentFqn.contains("<")) {
|
||||||
|
currentFqn = currentFqn.substring(0, currentFqn.indexOf('<'));
|
||||||
|
}
|
||||||
|
if (currentFqn.equals(normalizedExpectedType)
|
||||||
|
|| currentFqn.endsWith("." + normalizedExpectedType)
|
||||||
|
|| normalizedExpectedType.endsWith("." + currentFqn)) {
|
||||||
|
matched[0] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (matched[0]) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if (superFqn.equals(expectedType) || superFqn.endsWith("." + expectedType) || expectedType.endsWith("." + superFqn)) return true;
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
superFqn = superTd != null ? context.getSuperclassFqn(superTd) : null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List interfaces = java.util.Collections.emptyList();
|
List interfaces = java.util.Collections.emptyList();
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import java.util.LinkedHashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -379,17 +380,11 @@ public class CodebaseContext {
|
|||||||
String simpleName = td.getName().getIdentifier();
|
String simpleName = td.getName().getIdentifier();
|
||||||
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|
||||||
classes.put(fqn, (CompilationUnit) td.getRoot());
|
if (shouldReplaceTypeIndex(fqn, javaFile)) {
|
||||||
classPaths.put(fqn, javaFile);
|
classes.put(fqn, (CompilationUnit) td.getRoot());
|
||||||
typeDeclarations.put(fqn, td);
|
classPaths.put(fqn, javaFile);
|
||||||
|
typeDeclarations.put(fqn, td);
|
||||||
if (simpleNameToFqn.containsKey(simpleName)) {
|
registerSimpleNameForFqn(simpleName, fqn);
|
||||||
String existingFqn = simpleNameToFqn.get(simpleName);
|
|
||||||
if (!existingFqn.equals(fqn)) {
|
|
||||||
ambiguousSimpleNames.add(simpleName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
simpleNameToFqn.put(simpleName, fqn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively index nested types
|
// Recursively index nested types
|
||||||
@@ -452,23 +447,93 @@ public class CodebaseContext {
|
|||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void indexRecord(org.eclipse.jdt.core.dom.RecordDeclaration rd, String parentFqn, Path javaFile) {
|
/**
|
||||||
String simpleName = rd.getName().getIdentifier();
|
* Higher rank wins on duplicate FQN indexing: main sources beat build artifacts.
|
||||||
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
*/
|
||||||
|
static int sourcePathRank(Path path) {
|
||||||
|
if (path == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String normalized = path.toString().replace('\\', '/');
|
||||||
|
if (normalized.contains("/src/main/java/")) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (normalized.contains("/target/") || normalized.contains("/build/")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Treat as a class since it is a type
|
private boolean shouldReplaceTypeIndex(String fqn, Path javaFile) {
|
||||||
classes.put(fqn, (CompilationUnit) rd.getRoot());
|
Path existingPath = classPaths.get(fqn);
|
||||||
classPaths.put(fqn, javaFile);
|
if (existingPath == null) {
|
||||||
recordDeclarations.put(fqn, rd);
|
return true;
|
||||||
|
}
|
||||||
|
int existingRank = sourcePathRank(existingPath);
|
||||||
|
int newRank = sourcePathRank(javaFile);
|
||||||
|
if (newRank > existingRank) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (newRank < existingRank) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug(
|
||||||
|
"Skipping lower-rank duplicate type index fqn={} path={} rank={} kept={} rank={}",
|
||||||
|
fqn,
|
||||||
|
javaFile,
|
||||||
|
newRank,
|
||||||
|
existingPath,
|
||||||
|
existingRank);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerSimpleNameForFqn(String simpleName, String fqn) {
|
||||||
if (simpleNameToFqn.containsKey(simpleName)) {
|
if (simpleNameToFqn.containsKey(simpleName)) {
|
||||||
String existingFqn = simpleNameToFqn.get(simpleName);
|
String existingFqn = simpleNameToFqn.get(simpleName);
|
||||||
if (!existingFqn.equals(fqn)) {
|
if (existingFqn != null && !existingFqn.equals(fqn)) {
|
||||||
ambiguousSimpleNames.add(simpleName);
|
ambiguousSimpleNames.add(simpleName);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
simpleNameToFqn.put(simpleName, fqn);
|
simpleNameToFqn.put(simpleName, fqn);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk {@code start} then each superclass; stop on null, missing type, {@code java.lang.Object}, or cycle.
|
||||||
|
*/
|
||||||
|
public void forEachSuperclass(TypeDeclaration start, Consumer<TypeDeclaration> visitor) {
|
||||||
|
if (start == null || visitor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> visited = new LinkedHashSet<>();
|
||||||
|
TypeDeclaration current = start;
|
||||||
|
while (current != null) {
|
||||||
|
String fqn = getFqn(current);
|
||||||
|
if (fqn != null && !visited.add(fqn)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
visitor.accept(current);
|
||||||
|
String superFqn = getSuperclassFqn(current);
|
||||||
|
if (superFqn == null || "java.lang.Object".equals(superFqn)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = getTypeDeclaration(superFqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void indexRecord(org.eclipse.jdt.core.dom.RecordDeclaration rd, String parentFqn, Path javaFile) {
|
||||||
|
String simpleName = rd.getName().getIdentifier();
|
||||||
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|
||||||
|
if (shouldReplaceTypeIndex(fqn, javaFile)) {
|
||||||
|
// Treat as a class since it is a type
|
||||||
|
classes.put(fqn, (CompilationUnit) rd.getRoot());
|
||||||
|
classPaths.put(fqn, javaFile);
|
||||||
|
recordDeclarations.put(fqn, rd);
|
||||||
|
registerSimpleNameForFqn(simpleName, fqn);
|
||||||
|
}
|
||||||
|
|
||||||
// Recursively index nested types
|
// Recursively index nested types
|
||||||
for (Object decl : rd.bodyDeclarations()) {
|
for (Object decl : rd.bodyDeclarations()) {
|
||||||
@@ -612,23 +677,18 @@ public class CodebaseContext {
|
|||||||
String simpleName = ed.getName().getIdentifier();
|
String simpleName = ed.getName().getIdentifier();
|
||||||
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|
||||||
classes.put(fqn, (CompilationUnit) ed.getRoot());
|
if (shouldReplaceTypeIndex(fqn, javaFile)) {
|
||||||
classPaths.put(fqn, javaFile);
|
classes.put(fqn, (CompilationUnit) ed.getRoot());
|
||||||
|
classPaths.put(fqn, javaFile);
|
||||||
|
|
||||||
List<String> values = new ArrayList<>();
|
List<String> values = new ArrayList<>();
|
||||||
for (Object o : ed.enumConstants()) {
|
for (Object o : ed.enumConstants()) {
|
||||||
if (o instanceof EnumConstantDeclaration ecd) {
|
if (o instanceof EnumConstantDeclaration ecd) {
|
||||||
values.add(fqn + "." + ecd.getName().getIdentifier());
|
values.add(fqn + "." + ecd.getName().getIdentifier());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
enumValues.put(fqn, values);
|
||||||
enumValues.put(fqn, values);
|
registerSimpleNameForFqn(simpleName, fqn);
|
||||||
if (simpleNameToFqn.containsKey(simpleName)) {
|
|
||||||
String existingFqn = simpleNameToFqn.get(simpleName);
|
|
||||||
if (existingFqn != null && !existingFqn.equals(fqn)) {
|
|
||||||
ambiguousSimpleNames.add(simpleName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
simpleNameToFqn.put(simpleName, fqn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively index nested types inside enum body
|
// Recursively index nested types inside enum body
|
||||||
@@ -1009,7 +1069,21 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MethodDeclaration findMethodDeclarationLegacy(TypeDeclaration td, String methodName, boolean includeSuper) {
|
private MethodDeclaration findMethodDeclarationLegacy(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||||
if (td == null) return null;
|
return findMethodDeclarationLegacy(td, methodName, includeSuper, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findMethodDeclarationLegacy(
|
||||||
|
TypeDeclaration td,
|
||||||
|
String methodName,
|
||||||
|
boolean includeSuper,
|
||||||
|
Set<String> visited) {
|
||||||
|
if (td == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String typeFqn = getFqn(td);
|
||||||
|
if (typeFqn != null && !visited.add(typeFqn)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
for (MethodDeclaration method : td.getMethods()) {
|
for (MethodDeclaration method : td.getMethods()) {
|
||||||
if (method.getName().getIdentifier().equals(methodName)) {
|
if (method.getName().getIdentifier().equals(methodName)) {
|
||||||
return method;
|
return method;
|
||||||
@@ -1021,7 +1095,10 @@ public class CodebaseContext {
|
|||||||
if (superFqn != null) {
|
if (superFqn != null) {
|
||||||
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
||||||
if (superTd != null) {
|
if (superTd != null) {
|
||||||
return findMethodDeclarationLegacy(superTd, methodName, true);
|
MethodDeclaration found = findMethodDeclarationLegacy(superTd, methodName, true, visited);
|
||||||
|
if (found != null) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1029,10 +1106,14 @@ public class CodebaseContext {
|
|||||||
for (Object interfaceTypeObj : td.superInterfaceTypes()) {
|
for (Object interfaceTypeObj : td.superInterfaceTypes()) {
|
||||||
Type interfaceType = (Type) interfaceTypeObj;
|
Type interfaceType = (Type) interfaceTypeObj;
|
||||||
String interfaceName = extractTypeName(interfaceType);
|
String interfaceName = extractTypeName(interfaceType);
|
||||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null);
|
TypeDeclaration interfaceTd = getTypeDeclaration(
|
||||||
|
interfaceName,
|
||||||
|
(td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null);
|
||||||
if (interfaceTd != null) {
|
if (interfaceTd != null) {
|
||||||
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true);
|
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true, visited);
|
||||||
if (found != null) return found;
|
if (found != null) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -327,6 +329,142 @@ class CodebaseContextTest {
|
|||||||
.containsExactlyInAnyOrder("abcd.ccc.AaaListener", "abcd.ccc.Av");
|
.containsExactlyInAnyOrder("abcd.ccc.AaaListener", "abcd.ccc.Av");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getImplementationsShouldTerminateOnCircularExtends() throws IOException {
|
||||||
|
Files.createDirectories(tempDir.resolve("cycle"));
|
||||||
|
// Invalid Java inheritance cycle — still must not hang in collectImplementations / subtype walks.
|
||||||
|
Files.writeString(tempDir.resolve("cycle/A.java"), """
|
||||||
|
package cycle;
|
||||||
|
public class A extends B {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("cycle/B.java"), """
|
||||||
|
package cycle;
|
||||||
|
public class B extends A {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
assertThat(context.getImplementations("cycle.A")).contains("cycle.B");
|
||||||
|
assertThat(context.getImplementations("cycle.B")).contains("cycle.A");
|
||||||
|
assertThat(context.isSubtypeOrSame("cycle.A", "cycle.B")).isTrue();
|
||||||
|
assertThat(context.isSubtypeOrSame("cycle.B", "cycle.A")).isTrue();
|
||||||
|
assertThat(context.areClassesPolymorphicallyCompatible("cycle.A", "cycle.B")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forEachSuperclassShouldVisitEachTypeOnceOnCircularExtends() throws IOException {
|
||||||
|
Files.createDirectories(tempDir.resolve("cycle"));
|
||||||
|
Files.writeString(tempDir.resolve("cycle/A.java"), """
|
||||||
|
package cycle;
|
||||||
|
public class A extends B {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("cycle/B.java"), """
|
||||||
|
package cycle;
|
||||||
|
public class B extends A {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
List<String> visited = new ArrayList<>();
|
||||||
|
TypeDeclaration a = context.getTypeDeclaration("cycle.A");
|
||||||
|
context.forEachSuperclass(a, td -> visited.add(context.getFqn(td)));
|
||||||
|
|
||||||
|
assertThat(visited).containsExactly("cycle.A", "cycle.B");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findMethodDeclarationShouldTerminateOnCircularExtends() throws IOException {
|
||||||
|
Files.createDirectories(tempDir.resolve("cycle"));
|
||||||
|
Files.writeString(tempDir.resolve("cycle/Base.java"), """
|
||||||
|
package cycle;
|
||||||
|
public class Base {
|
||||||
|
public void handle() {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("cycle/A.java"), """
|
||||||
|
package cycle;
|
||||||
|
public class A extends B {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("cycle/B.java"), """
|
||||||
|
package cycle;
|
||||||
|
public class B extends A {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration a = context.getTypeDeclaration("cycle.A");
|
||||||
|
assertThat(context.findMethodDeclaration(a, "handle", true)).isNull();
|
||||||
|
assertThat(context.findMethodDeclaration(a, "missing", true)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexShouldPreferMainSourceOverGeneratedStubForSameFqn() throws IOException {
|
||||||
|
Path module = tempDir.resolve("module");
|
||||||
|
Path mainFoo = module.resolve("src/main/java/com/example/Foo.java");
|
||||||
|
Path stubFoo = module.resolve("target/generated-sources/annotations/com/example/Foo.java");
|
||||||
|
Files.createDirectories(mainFoo.getParent());
|
||||||
|
Files.createDirectories(stubFoo.getParent());
|
||||||
|
Files.writeString(mainFoo, """
|
||||||
|
package com.example;
|
||||||
|
public class Foo {
|
||||||
|
public String realBody() { return "main"; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(stubFoo, """
|
||||||
|
package com.example;
|
||||||
|
public class Foo {
|
||||||
|
public String stubBody() { return "stub"; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(module);
|
||||||
|
|
||||||
|
assertThat(context.getPath("com.example.Foo").toString()).contains("src/main/java");
|
||||||
|
assertThat(context.getTypeDeclaration("com.example.Foo").getMethods())
|
||||||
|
.extracting(md -> md.getName().getIdentifier())
|
||||||
|
.contains("realBody")
|
||||||
|
.doesNotContain("stubBody");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexShouldPreferMainSourceOverGeneratedStubRegardlessOfScanRootOrder() throws IOException {
|
||||||
|
Path module = tempDir.resolve("module");
|
||||||
|
Path mainFoo = module.resolve("src/main/java/com/example/Foo.java");
|
||||||
|
Path stubFoo = module.resolve("target/generated-sources/annotations/com/example/Foo.java");
|
||||||
|
Files.createDirectories(mainFoo.getParent());
|
||||||
|
Files.createDirectories(stubFoo.getParent());
|
||||||
|
Files.writeString(mainFoo, """
|
||||||
|
package com.example;
|
||||||
|
public class Foo { public void realBody() {} }
|
||||||
|
""");
|
||||||
|
Files.writeString(stubFoo, """
|
||||||
|
package com.example;
|
||||||
|
public class Foo { public void stubBody() {} }
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext generatedFirst = new CodebaseContext();
|
||||||
|
generatedFirst.scan(Set.of(
|
||||||
|
module.resolve("target/generated-sources"),
|
||||||
|
module.resolve("src/main/java")), Set.of());
|
||||||
|
|
||||||
|
CodebaseContext mainFirst = new CodebaseContext();
|
||||||
|
mainFirst.scan(Set.of(
|
||||||
|
module.resolve("src/main/java"),
|
||||||
|
module.resolve("target/generated-sources")), Set.of());
|
||||||
|
|
||||||
|
for (CodebaseContext context : List.of(generatedFirst, mainFirst)) {
|
||||||
|
assertThat(context.getPath("com.example.Foo").toString()).contains("src/main/java");
|
||||||
|
assertThat(context.getTypeDeclaration("com.example.Foo").getMethods())
|
||||||
|
.extracting(md -> md.getName().getIdentifier())
|
||||||
|
.contains("realBody")
|
||||||
|
.doesNotContain("stubBody");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getImplementationsShouldCanonicalizeImportAwareExtendsToSameFqnBucket() throws IOException {
|
void getImplementationsShouldCanonicalizeImportAwareExtendsToSameFqnBucket() throws IOException {
|
||||||
Files.createDirectories(tempDir.resolve("abcd/ccc"));
|
Files.createDirectories(tempDir.resolve("abcd/ccc"));
|
||||||
|
|||||||
Reference in New Issue
Block a user