edge case fixes

This commit is contained in:
2026-06-21 18:45:24 +02:00
parent cbb00e8a0f
commit 32de0a51c6
2 changed files with 154 additions and 5 deletions

View File

@@ -261,6 +261,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (varName != null && declaredType == null) {
for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName);
System.out.println("DEBUG getVariableDeclaredType(" + methodFqn + ", " + varName + ") = " + declaredType);
if (declaredType != null) {
sourceMethod = methodFqn;
break;
@@ -383,11 +384,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
for (Object pObj : md.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
if (svd.getName().getIdentifier().equals(varName)) {
if (svd.getName().getIdentifier().equals(cleanFieldName)) {
return svd.getType().toString();
}
}
@@ -398,15 +400,71 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
foundType[0] = node.getType().toString();
}
}
return super.visit(node);
}
@Override
public boolean visit(org.eclipse.jdt.core.dom.LambdaExpression node) {
for (Object paramObj : node.parameters()) {
org.eclipse.jdt.core.dom.VariableDeclaration param = (org.eclipse.jdt.core.dom.VariableDeclaration) paramObj;
if (param.getName().getIdentifier().equals(cleanFieldName)) {
if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
foundType[0] = svd.getType().toString();
return false;
}
org.eclipse.jdt.core.dom.ASTNode parent = node.getParent();
if (parent instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
org.eclipse.jdt.core.dom.Expression caller = mi.getExpression();
if (caller instanceof org.eclipse.jdt.core.dom.SimpleName callerSn) {
String callerName = callerSn.getIdentifier();
if (!callerName.equals(cleanFieldName)) {
String callerType = getVariableDeclaredType(methodFqn, callerName);
if (callerType != null && callerType.contains("<")) {
int start = callerType.indexOf('<');
int end = callerType.lastIndexOf('>');
if (start >= 0 && end > start) {
foundType[0] = callerType.substring(start + 1, end);
return false;
}
}
}
} else if (caller instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
String callerName = fa.getName().getIdentifier();
if (!callerName.equals(cleanFieldName)) {
String callerType = getVariableDeclaredType(methodFqn, callerName);
if (callerType != null && callerType.contains("<")) {
int start = callerType.indexOf('<');
int end = callerType.lastIndexOf('>');
if (start >= 0 && end > start) {
foundType[0] = callerType.substring(start + 1, end);
return false;
}
}
}
}
}
}
}
return super.visit(node);
}
});
}
return foundType[0];
if (foundType[0] != null) return foundType[0];
}
// Fallback to fields
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
return fd.getType().toString();
}
}
}
}
return null;
@@ -618,8 +676,26 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
// 2. Inline Instantiation Getter (e.g. new Event(CONSTANT).getType())
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
for (Object argObj : cic.arguments()) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) argObj, constants);
if (cic.getAnonymousClassDeclaration() != null) {
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
if (declObj instanceof org.eclipse.jdt.core.dom.MethodDeclaration mdecl) {
if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) {
mdecl.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
if (rs.getExpression() != null) {
extractConstantsFromExpression(rs.getExpression(), constants);
}
return super.visit(rs);
}
});
}
}
}
} else {
for (Object argObj : cic.arguments()) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) argObj, constants);
}
}
}

View File

@@ -2253,4 +2253,77 @@ class HeuristicCallGraphEngineTest {
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_FIELD");
}
@Test
void shouldResolveEventFromLambda() throws IOException {
String source = """
package com.example;
import java.util.List;
public class OrderController {
private OrderService service;
public void handleList(List<LambdaEvent> events) {
events.forEach(e -> service.process(e.getType()));
}
}
class LambdaEvent {
private String type;
public LambdaEvent(String typeArg) { this.type = typeArg; }
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_lambda");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleList").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
System.out.println("DEBUG LAMBDA CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents()));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).isNullOrEmpty();
}
@Test
void shouldResolveEventFromAnonymousClass() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new EventProvider() {
@Override
public String getEvent() { return "ANONYMOUS_EVENT"; }
}.getEvent());
}
}
interface EventProvider {
String getEvent();
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_anonymous");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ANONYMOUS_EVENT");
}
}