3 Commits

4 changed files with 375 additions and 59 deletions

View File

@@ -144,6 +144,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
System.out.println("DEBUG tracedVar: " + tracedVar + " for currentParamName: " + currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
tracedVar = extractedFinalTraced[0];
@@ -260,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;
@@ -307,8 +309,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
}
}
// (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly
// for string literals or enum-like constants.
boolean hasValidConstant = false;
for (String ev : polymorphicEvents) {
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
@@ -318,36 +320,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
}
if (!hasValidConstant && resolvedValue.contains("(")) {
java.util.List<String> scraped = new java.util.ArrayList<>();
// Extract "STRING_LITERALS"
java.util.regex.Matcher m1 = java.util.regex.Pattern.compile("\"([^\"]+)\"").matcher(resolvedValue);
while (m1.find()) {
scraped.add(m1.group(1));
}
// Extract ENUM_LIKE_CONSTANTS
java.util.regex.Matcher m2 = java.util.regex.Pattern.compile("\\b([A-Z_]{3,})\\b").matcher(resolvedValue);
while (m2.find()) {
if (!scraped.contains(m2.group(1))) {
scraped.add(m2.group(1));
}
}
if (!scraped.isEmpty()) {
polymorphicEvents.clear();
polymorphicEvents.addAll(scraped);
}
// As a fallback, attempt direct AST extraction (e.g. from MessageBuilder chains or inline constructor args)
if (!hasValidConstant && exprNode instanceof org.eclipse.jdt.core.dom.Expression) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) exprNode, polymorphicEvents);
}
// Clean up any remaining invalid AST fallbacks (like 'type', 'event') if we found valid ones
if (polymorphicEvents.size() > 1) {
polymorphicEvents.removeIf(e -> {
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e;
return !val.equals(val.toUpperCase()) || val.length() <= 2;
});
}
// The aggressive regex scraper has been completely removed to avoid false positives.
// We rely on precise AST traversal instead.
List<String> newPolyEvents = new ArrayList<>();
for (String pe : polymorphicEvents) {
List<String> resolved = resolveClassConstantReturns(pe, context, null);
@@ -359,6 +339,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
polymorphicEvents = newPolyEvents;
// Clean up any remaining invalid AST fallbacks (like 'type', 'event', or unresolved class names)
polymorphicEvents.removeIf(e -> {
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e;
return !val.equals(val.toUpperCase()) || val.length() <= 2;
});
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
.event(resolvedValue)
@@ -398,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();
}
}
@@ -413,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;
@@ -631,7 +674,44 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
}
// 2. Delegate to method return analysis
// 2. Inline Instantiation Getter (e.g. new Event(CONSTANT).getType())
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
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);
}
}
}
// 3. Builder Pattern (e.g. MessageBuilder.withPayload(...).setHeader("event", CONSTANT).build())
org.eclipse.jdt.core.dom.Expression current = mi;
while (current instanceof org.eclipse.jdt.core.dom.MethodInvocation chainMi) {
String chainName = chainMi.getName().getIdentifier();
if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(1), constants);
} else if (chainName.equals("event") && chainMi.arguments().size() == 1) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(0), constants);
}
current = chainMi.getExpression();
}
// 4. Delegate to method return analysis
TypeDeclaration td = findEnclosingType(mi);
if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression)) {
List<String> values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new java.util.HashSet<>(), null);
@@ -693,9 +773,55 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (expr instanceof SimpleName sn) {
return sn.getIdentifier();
}
return expr.toString();
return initializer[0].toString();
}
}
// If local variable not found, check field assignments
String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName;
final Expression[] fieldInitializer = new Expression[1];
ASTVisitor fieldVisitor = new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) {
fieldInitializer[0] = node.getInitializer();
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
Expression left = node.getLeftHandSide();
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) ||
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) {
fieldInitializer[0] = node.getRightHandSide();
}
return super.visit(node);
}
};
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) {
fieldInitializer[0] = frag.getInitializer();
}
}
}
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
for (MethodDeclaration classMd : td.getMethods()) {
if (classMd.isConstructor() && classMd.getBody() != null) {
classMd.getBody().accept(fieldVisitor);
}
}
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
for (MethodDeclaration classMd : td.getMethods()) {
if (!classMd.isConstructor() && classMd.getBody() != null) {
classMd.getBody().accept(fieldVisitor);
}
}
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
}
return null;
}
@@ -792,8 +918,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
protected String[] extractMethodSuffix(String paramName, String currentSuffix) {
int dotIndex = paramName.indexOf('.');
int dotIndex = paramName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex + 1 < paramName.length() && Character.isLowerCase(paramName.charAt(dotIndex + 1))) {
if (paramName.substring(0, dotIndex).equals("this") && !paramName.endsWith("()")) {
return new String[] { paramName, currentSuffix };
}
return new String[] { paramName.substring(0, dotIndex), paramName.substring(dotIndex) + currentSuffix };
}
return new String[] { paramName, currentSuffix };

View File

@@ -2134,5 +2134,196 @@ class HeuristicCallGraphEngineTest {
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
}
@Test
void shouldResolveEventFromConstructorAssignment() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
service.process(e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_constructor_assignment");
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("EVENT_FROM_CONSTRUCTOR");
}
@Test
void shouldPrioritizeSetterOverConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
e.setType("EVENT_FROM_SETTER");
service.process(e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public void setType(String t) { this.type = t; }
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_setter_priority");
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("EVENT_FROM_SETTER");
}
@Test
void shouldResolveEventFromFieldConstructorAssignment() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
private EventWithConstructorArg e;
public OrderController() {
this.e = new EventWithConstructorArg("EVENT_FROM_FIELD", 123);
}
public void handleStatus() {
service.process(this.e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_assignment");
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));
System.out.println("DEBUG 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()).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");
}
}

View File

@@ -381,10 +381,18 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvents.PAY", "OrderEvents.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
}, {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.CANCEL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
@@ -400,7 +408,7 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "event",
"event" : "new PayEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
@@ -408,25 +416,13 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : null
"polymorphicEvents" : [ "OrderEvents.PAY" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
}, {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.FULFILLED",
"event" : "OrderEvents.FULFILL"
}, {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.CANCEL"
}, {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.ABCD"
} ]
}, {
"entryPoint" : {

View File

@@ -144,7 +144,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -175,7 +175,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -206,7 +206,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -241,7 +241,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -276,7 +276,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 78,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -311,7 +311,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -346,7 +346,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 50,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {