8 Commits

12 changed files with 1524 additions and 147 deletions

View File

@@ -88,6 +88,13 @@ public class ConstantResolver {
} }
} }
if ("valueOf".equals(mi.getName().getIdentifier()) && mi.getExpression() != null) {
java.util.List<String> enumValues = context.getEnumValues(mi.getExpression().toString());
if (enumValues != null && !enumValues.isEmpty()) {
return "ENUM_SET:" + String.join(",", enumValues);
}
}
IMethodBinding mb = mi.resolveMethodBinding(); IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) { if (mb != null) {
ITypeBinding returnType = mb.getReturnType(); ITypeBinding returnType = mb.getReturnType();

View File

@@ -144,6 +144,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
String tracedVar = traceLocalVariable(entryMethod, currentParamName); String tracedVar = traceLocalVariable(entryMethod, currentParamName);
System.out.println("DEBUG tracedVar: " + tracedVar + " for currentParamName: " + currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) { if (tracedVar != null && !tracedVar.equals(currentParamName)) {
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix); String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
tracedVar = extractedFinalTraced[0]; tracedVar = extractedFinalTraced[0];
@@ -174,10 +175,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
// Parse resolvedValue using JDT to robustly handle complex expressions // Parse resolvedValue using JDT to robustly handle complex expressions
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.getJLSLatest()); System.out.println("DEBUG Parsing resolvedValue: " + resolvedValue);
exprParser.setSource(resolvedValue.toCharArray()); org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17);
exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION); exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
exprParser.setSource(resolvedValue.toCharArray());
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null); org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null);
System.out.println("DEBUG exprNode: " + (exprNode != null ? exprNode.getClass().getName() : "null") + ", node: " + exprNode);
String varName = null; String varName = null;
String methodName = null; String methodName = null;
@@ -186,6 +189,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
methodName = mi.getName().getIdentifier(); methodName = mi.getName().getIdentifier();
System.out.println("DEBUG mi.getName(): " + methodName + ", mi.getExpression() type: " + (mi.getExpression() != null ? mi.getExpression().getClass().getName() : "null"));
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier(); varName = sn.getIdentifier();
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe && } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
@@ -195,8 +199,41 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
declaredType = ce.getType().toString(); declaredType = ce.getType().toString();
sourceMethod = "inline-cast"; sourceMethod = "inline-cast";
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
System.out.println("DEBUG found ClassInstanceCreation!");
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation"; sourceMethod = "inline-instantiation";
if (cic.getAnonymousClassDeclaration() != null) {
System.out.println("DEBUG resolving anonymous class!");
final List<String> polyEventsRef = polymorphicEvents;
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
if (declObj instanceof org.eclipse.jdt.core.dom.MethodDeclaration mDecl && mDecl.getName().getIdentifier().equals(methodName) && mDecl.getBody() != null) {
System.out.println("DEBUG visiting anonymous class method: " + methodName);
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) {
System.out.println("DEBUG found return expression: " + rs.getExpression().toString());
List<org.eclipse.jdt.core.dom.Expression> tracedReturns = traceVariableAll(rs.getExpression());
for (org.eclipse.jdt.core.dom.Expression tracedRet : tracedReturns) {
extractConstantsFromExpression(tracedRet, polyEventsRef);
}
}
return super.visit(rs);
}
});
}
}
}
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) {
methodName = innerMi.getName().getIdentifier();
if (innerMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName snInner) {
varName = snInner.getIdentifier();
} else {
String exprStr = innerMi.getExpression() != null ? innerMi.getExpression().toString() : "";
if (!exprStr.contains("(")) {
varName = exprStr;
}
}
} else { } else {
// Fallback for complex chained expressions // Fallback for complex chained expressions
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : ""; String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "";
@@ -261,6 +298,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
for (String methodFqn : path) { for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName); declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) { if (declaredType != null) {
System.out.println("DEBUG getVariableDeclaredType(" + methodFqn + ", " + varName + ") = " + declaredType);
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
List<String> values = resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu);
System.out.println("DEBUG resolveMethodReturnConstant returned: " + values);
if (values != null && !values.isEmpty()) {
polymorphicEvents.addAll(values);
}
sourceMethod = methodFqn; sourceMethod = methodFqn;
break; break;
} }
@@ -271,44 +316,73 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (staticTd != null) { if (staticTd != null) {
declaredType = context.getFqn(staticTd); declaredType = context.getFqn(staticTd);
sourceMethod = "static-call"; sourceMethod = "static-call";
} else if (context.getEnumValues(varName) != null) {
declaredType = varName;
sourceMethod = "static-call";
}
}
}
if (declaredType == null && methodName != null && !methodName.equals("VariableReference")) {
System.out.println("DEBUG fallback triggered for methodName: " + methodName);
for (String methodFqn : path) {
System.out.println("DEBUG fallback checking path method: " + methodFqn);
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
if (currentTd != null && context.findMethodDeclaration(currentTd, methodName, true) != null) {
System.out.println("DEBUG fallback found method in " + context.getFqn(currentTd));
CompilationUnit cu = currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
List<String> values = resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu);
System.out.println("DEBUG fallback resolveMethodReturnConstant returned: " + values);
if (values != null && !values.isEmpty()) {
polymorphicEvents.addAll(values);
sourceMethod = methodFqn;
break;
}
} }
} }
} }
if (declaredType != null) { if (declaredType != null) {
List<String> typesToInspect = new ArrayList<>(); List<String> enumValues = context.getEnumValues(declaredType);
if ("inline-instantiation".equals(sourceMethod)) { if (enumValues != null && !enumValues.isEmpty()) {
typesToInspect.add(declaredType); for (String ev : enumValues) {
} else { polymorphicEvents.add(parseEnumSetElement(ev));
typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType));
}
for (String type : typesToInspect) {
Set<String> visited = new HashSet<>();
TypeDeclaration baseTd = context.getTypeDeclaration(type);
CompilationUnit cuToUse = null;
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) baseTd.getRoot();
} else {
String firstPathMethod = path.get(0);
String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) entryTd.getRoot();
}
} }
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse); } else {
for (String constant : constants) { List<String> typesToInspect = new ArrayList<>();
if (!polymorphicEvents.contains(constant)) { if ("inline-instantiation".equals(sourceMethod)) {
polymorphicEvents.add(constant); typesToInspect.add(declaredType);
} else {
typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType));
}
for (String type : typesToInspect) {
Set<String> visited = new HashSet<>();
TypeDeclaration baseTd = context.getTypeDeclaration(type);
CompilationUnit cuToUse = null;
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) baseTd.getRoot();
} else {
String firstPathMethod = path.get(0);
String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) entryTd.getRoot();
}
}
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
for (String constant : constants) {
if (!polymorphicEvents.contains(constant)) {
polymorphicEvents.add(constant);
}
} }
} }
} }
} }
} }
// (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; boolean hasValidConstant = false;
for (String ev : polymorphicEvents) { for (String ev : polymorphicEvents) {
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev; String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
@@ -318,36 +392,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
} }
if (!hasValidConstant && resolvedValue.contains("(")) { // As a fallback, attempt direct AST extraction (e.g. from MessageBuilder chains or inline constructor args)
java.util.List<String> scraped = new java.util.ArrayList<>(); if (!hasValidConstant && exprNode instanceof org.eclipse.jdt.core.dom.Expression) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) exprNode, polymorphicEvents);
// 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);
}
} }
// Clean up any remaining invalid AST fallbacks (like 'type', 'event') if we found valid ones // The aggressive regex scraper has been completely removed to avoid false positives.
if (polymorphicEvents.size() > 1) { // We rely on precise AST traversal instead.
polymorphicEvents.removeIf(e -> {
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e;
return !val.equals(val.toUpperCase()) || val.length() <= 2;
});
}
List<String> newPolyEvents = new ArrayList<>(); List<String> newPolyEvents = new ArrayList<>();
for (String pe : polymorphicEvents) { for (String pe : polymorphicEvents) {
List<String> resolved = resolveClassConstantReturns(pe, context, null); List<String> resolved = resolveClassConstantReturns(pe, context, null);
@@ -359,6 +411,15 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
polymorphicEvents = newPolyEvents; polymorphicEvents = newPolyEvents;
// Clean up any remaining invalid AST fallbacks (like 'type', 'event', or unresolved class names)
polymorphicEvents.removeIf(e -> {
if (e.contains(".")) return false; // If it's a fully qualified enum/constant, keep it
String val = e;
return !val.equals(val.toUpperCase()) || val.length() <= 1;
});
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) { if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder() return TriggerPoint.builder()
.event(resolvedValue) .event(resolvedValue)
@@ -392,37 +453,148 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return -1; return -1;
} }
protected String getVariableDeclaredType(String methodFqn, String varName) { public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
if (methodFqn == null || !methodFqn.contains(".")) return null; System.out.println("DEBUG getVariableDeclaredType called with: " + methodFqn + ", " + cleanFieldName);
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); if (methodFqn == null) return null;
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); int lastDot = methodFqn.lastIndexOf('.');
TypeDeclaration td = context.getTypeDeclaration(className); if (lastDot > 0) {
if (td != null) { String fqn = methodFqn.substring(0, lastDot);
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); String methodName = methodFqn.substring(lastDot + 1);
if (md != null) { TypeDeclaration td = context.getTypeDeclaration(fqn);
for (Object pObj : md.parameters()) { System.out.println("DEBUG getTypeDeclaration for " + fqn + " = " + (td != null));
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj; if (td != null) {
if (svd.getName().getIdentifier().equals(varName)) { MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
return svd.getType().toString(); System.out.println("DEBUG findMethodDeclaration for " + methodName + " = " + (md != null));
if (md != null) {
for (Object pObj : md.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
if (svd.getName().getIdentifier().equals(cleanFieldName)) {
return svd.getType().toString();
}
} }
} final String[] foundType = new String[1];
final String[] foundType = new String[1]; if (md.getBody() != null) {
if (md.getBody() != null) { System.out.println("DEBUG visiting body for " + cleanFieldName);
md.getBody().accept(new ASTVisitor() { md.getBody().accept(new ASTVisitor() {
@Override @Override
public boolean visit(VariableDeclarationStatement node) { public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) { for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) { if (frag.getName().getIdentifier().equals(cleanFieldName)) {
foundType[0] = node.getType().toString(); foundType[0] = node.getType().toString();
} }
} }
return super.visit(node); 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;
System.out.println("DEBUG visiting lambda param: " + param.getName().getIdentifier());
if (param.getName().getIdentifier().equals(cleanFieldName)) {
System.out.println("DEBUG found lambda param: " + cleanFieldName);
if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
foundType[0] = svd.getType().toString();
System.out.println("DEBUG lambda explicit type: " + foundType[0]);
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();
// Phase 6: Stream API chains
boolean hasMap = false;
while (caller instanceof org.eclipse.jdt.core.dom.MethodInvocation chainMi) {
String mName = chainMi.getName().getIdentifier();
if (mName.equals("map") || mName.equals("flatMap")) {
hasMap = true;
// Extract return type of the map lambda
if (chainMi.arguments().size() == 1 && chainMi.arguments().get(0) instanceof org.eclipse.jdt.core.dom.LambdaExpression lambda) {
if (lambda.getBody() instanceof org.eclipse.jdt.core.dom.MethodInvocation mapMi) {
// Just a heuristic for now: we use the method's return type if we could resolve it, but since we can't easily,
// we'll try to find the type of the caller and look up the method.
org.eclipse.jdt.core.dom.Expression mapCaller = mapMi.getExpression();
if (mapCaller instanceof org.eclipse.jdt.core.dom.SimpleName mapSn) {
String mapCallerType = getVariableDeclaredType(methodFqn, mapSn.getIdentifier());
if (mapCallerType != null) {
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
TypeDeclaration td = context.getTypeDeclaration(mapCallerType, cu);
if (td != null) {
MethodDeclaration mapMd = context.findMethodDeclaration(td, mapMi.getName().getIdentifier(), true);
if (mapMd != null && mapMd.getReturnType2() != null) {
foundType[0] = mapMd.getReturnType2().toString();
return false;
}
}
}
}
}
}
}
if (mName.equals("stream") || mName.equals("filter") || mName.equals("map") ||
mName.equals("flatMap") || mName.equals("peek") || mName.equals("distinct") ||
mName.equals("sorted") || mName.equals("limit") || mName.equals("skip")) {
caller = chainMi.getExpression();
} else {
break;
}
}
if (!hasMap) {
if (caller instanceof org.eclipse.jdt.core.dom.SimpleName callerSn) {
String callerName = callerSn.getIdentifier();
System.out.println("DEBUG callerName: " + callerName + " cleanFieldName: " + cleanFieldName);
if (!callerName.equals(cleanFieldName)) {
String callerType = getVariableDeclaredType(methodFqn, callerName);
System.out.println("DEBUG callerType for " + callerName + " = " + callerType);
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);
System.out.println("DEBUG foundType[0]: " + foundType[0]);
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; return null;
} }
@@ -507,15 +679,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited); List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
if (!consts.isEmpty()) { if (!consts.isEmpty()) {
constants.addAll(consts); constants.addAll(consts);
} else {
constants.add(sn.toString());
} }
} else if (tracedRetExpr instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { } else if (tracedRetExpr instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
List<String> consts = traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited); List<String> consts = traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited);
if (!consts.isEmpty()) { if (!consts.isEmpty()) {
constants.addAll(consts); constants.addAll(consts);
} else {
constants.add(fa.getName().getIdentifier());
} }
} }
} }
@@ -524,6 +692,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return super.visit(node); return super.visit(node);
} }
}); });
} else {
List<String> impls = context.getImplementations(className);
if (impls != null) {
for (String implName : impls) {
List<String> delegationResult = resolveMethodReturnConstant(implName, methodName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
}
}
} }
} }
visited.remove(fqn); visited.remove(fqn);
@@ -587,9 +763,38 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return super.visit(rs); return super.visit(rs);
} }
}); });
} else if (expr instanceof org.eclipse.jdt.core.dom.ArrayAccess aa) {
extractConstantsFromExpression(aa.getArray(), constants);
} else if (expr instanceof org.eclipse.jdt.core.dom.ArrayCreation ac && ac.getInitializer() != null) {
for (Object expObj : ac.getInitializer().expressions()) {
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) expObj, constants);
}
} else if (expr instanceof org.eclipse.jdt.core.dom.ArrayInitializer ai) {
for (Object expObj : ai.expressions()) {
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) expObj, constants);
}
} else if (expr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
for (Object argObj : cic.arguments()) {
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants);
}
} else if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { } else if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
String methodName = mi.getName().getIdentifier(); String methodName = mi.getName().getIdentifier();
// Phase 7: List.get(0) indexing
if (methodName.equals("get") && mi.arguments().size() == 1 && mi.getExpression() != null) {
extractConstantsFromExpression(mi.getExpression(), constants);
return;
}
// Phase 7: List.of("A", "B") or Arrays.asList("A", "B")
if ((methodName.equals("of") || methodName.equals("asList")) && mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName) {
for (Object argObj : mi.arguments()) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) argObj, constants);
}
return;
}
// 1. Local setter tracking // 1. Local setter tracking
if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
String varName = sn.getIdentifier(); String varName = sn.getIdentifier();
@@ -631,7 +836,59 @@ 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 {
boolean handledByLombok = false;
if (methodName.startsWith("get") && methodName.length() > 3) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
TypeDeclaration typeDecl = context.getTypeDeclaration(typeName);
if (typeDecl != null) {
String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
int fieldIndex = findConstructorArgumentIndexForLombokField(typeDecl, fieldName);
if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) {
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) cic.arguments().get(fieldIndex), constants);
handledByLombok = true;
}
}
}
if (!handledByLombok) {
for (Object argObj : cic.arguments()) {
extractConstantsFromArgument((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 currentMi = mi;
while (currentMi 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.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(0), constants);
}
currentMi = chainMi.getExpression();
}
// 4. Delegate to method return analysis
TypeDeclaration td = findEnclosingType(mi); TypeDeclaration td = findEnclosingType(mi);
if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression)) { 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); List<String> values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new java.util.HashSet<>(), null);
@@ -640,6 +897,72 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
} }
private int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) {
boolean isAllArgsConstructor = false;
boolean isRequiredArgsConstructor = false;
boolean isData = false;
boolean isValue = false;
for (Object modObj : td.modifiers()) {
if (modObj instanceof org.eclipse.jdt.core.dom.MarkerAnnotation ma) {
String name = ma.getTypeName().getFullyQualifiedName();
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
} else if (modObj instanceof org.eclipse.jdt.core.dom.NormalAnnotation na) {
String name = na.getTypeName().getFullyQualifiedName();
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
}
}
if (!isAllArgsConstructor && !isRequiredArgsConstructor && !isData && !isValue) {
return -1;
}
int index = 0;
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
boolean isStatic = false;
boolean isFinal = false;
boolean hasNonNull = false;
for (Object modObj : fd.modifiers()) {
if (modObj instanceof org.eclipse.jdt.core.dom.Modifier mod) {
if (mod.isStatic()) isStatic = true;
if (mod.isFinal()) isFinal = true;
} else if (modObj instanceof org.eclipse.jdt.core.dom.MarkerAnnotation ma) {
if (ma.getTypeName().getFullyQualifiedName().endsWith("NonNull")) hasNonNull = true;
}
}
if (isStatic) continue;
if (isRequiredArgsConstructor && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
if (isData && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
for (Object fragObj : fd.fragments()) {
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(fieldName)) {
return index;
}
index++;
}
}
return -1;
}
private void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List<String> constants) {
if (argObj instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation innerCic) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
if ("String".equals(typeName)) {
extractConstantsFromExpression(argObj, constants);
}
} else {
extractConstantsFromExpression(argObj, constants);
}
}
protected org.eclipse.jdt.core.dom.Block findEnclosingBlock(org.eclipse.jdt.core.dom.ASTNode node) { protected org.eclipse.jdt.core.dom.Block findEnclosingBlock(org.eclipse.jdt.core.dom.ASTNode node) {
org.eclipse.jdt.core.dom.ASTNode parent = node.getParent(); org.eclipse.jdt.core.dom.ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof org.eclipse.jdt.core.dom.Block)) { while (parent != null && !(parent instanceof org.eclipse.jdt.core.dom.Block)) {
@@ -656,46 +979,105 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (td != null) { if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) { if (md != null && md.getBody() != null) {
final Expression[] initializer = new Expression[1]; List<Expression> initializers = new ArrayList<>();
md.getBody().accept(new ASTVisitor() { md.getBody().accept(new ASTVisitor() {
@Override @Override
public boolean visit(VariableDeclarationFragment node) { public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializer[0] = node.getInitializer(); initializers.add(node.getInitializer());
} }
return super.visit(node); return super.visit(node);
} }
@Override @Override
public boolean visit(Assignment node) { public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
initializer[0] = node.getRightHandSide(); initializers.add(node.getRightHandSide());
} }
return super.visit(node); return super.visit(node);
} }
}); });
if (initializer[0] != null) { if (!initializers.isEmpty()) {
Expression expr = traceVariable(initializer[0]); List<String> stringified = new ArrayList<>();
if (expr instanceof MethodInvocation mi) { for (Expression expr : initializers) {
// Unwrapper logic: If wrapper method is called, extract its arguments recursively Expression traced = traceVariable(expr);
Expression innerMost = unwrapMethodInvocation(mi, 0); if (traced instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
if (innerMost instanceof MethodInvocation innerMi) { Expression innerMost = unwrapMethodInvocation(mi, 0);
if (innerMi.getExpression() instanceof SimpleName sn) { if (innerMost instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) {
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()"; if (innerMi.getExpression() instanceof SimpleName sn) {
stringified.add(sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()");
} else {
stringified.add(innerMi.getName().getIdentifier() + "()");
}
} else if (innerMost instanceof SimpleName sn) {
stringified.add(sn.getIdentifier());
} else {
stringified.add(innerMost.toString());
} }
return innerMi.getName().getIdentifier() + "()"; } else if (traced instanceof SimpleName sn) {
stringified.add(sn.getIdentifier());
} else if (traced instanceof org.eclipse.jdt.core.dom.ArrayInitializer) {
stringified.add("new Object[]" + traced.toString());
} else {
stringified.add(traced.toString());
} }
if (innerMost instanceof SimpleName sn) {
return sn.getIdentifier();
}
return innerMost.toString();
} }
if (expr instanceof SimpleName sn) { if (stringified.size() == 1) return stringified.get(0);
return sn.getIdentifier();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < stringified.size() - 1; i++) {
sb.append("true ? ").append(stringified.get(i)).append(" : ");
} }
return expr.toString(); sb.append(stringified.get(stringified.size() - 1));
return sb.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; return null;
} }
@@ -746,11 +1128,31 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return null; return null;
} }
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) { protected Expression unwrapMethodInvocation(org.eclipse.jdt.core.dom.MethodInvocation mi, int depth) {
if (depth > 5) return mi; if (depth > 5) return mi;
// Do not unwrap if called on an object (e.g. mapper.toEvent)
// Exceptions: Optional.of, Optional.ofNullable, Stream, etc.
if (mi.getExpression() != null) {
String exprStr = mi.getExpression().toString();
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) {
return mi;
}
}
if (!mi.arguments().isEmpty()) { if (!mi.arguments().isEmpty()) {
String mName = mi.getName().getIdentifier();
String lowerName = mName.toLowerCase();
// Do not unwrap methods that explicitly convert, map, parse, or create
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
lowerName.startsWith("create") || lowerName.startsWith("from")) {
return mi;
}
Expression arg = (Expression) mi.arguments().get(0); Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof MethodInvocation innerMi) { if (arg instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) {
return unwrapMethodInvocation(innerMi, depth + 1); return unwrapMethodInvocation(innerMi, depth + 1);
} }
return arg; return arg;
@@ -792,9 +1194,22 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
protected String[] extractMethodSuffix(String paramName, String currentSuffix) { protected String[] extractMethodSuffix(String paramName, String currentSuffix) {
int dotIndex = paramName.indexOf('.'); if (paramName != null && !paramName.contains(" ") && !paramName.startsWith("new ")) {
if (dotIndex > 0 && dotIndex + 1 < paramName.length() && Character.isLowerCase(paramName.charAt(dotIndex + 1))) { int bracketIndex = paramName.indexOf('[');
return new String[] { paramName.substring(0, dotIndex), paramName.substring(dotIndex) + currentSuffix }; int dotIndex = paramName.indexOf('.');
if (bracketIndex > 0 && (dotIndex == -1 || bracketIndex < dotIndex)) {
return new String[] { paramName.substring(0, bracketIndex), paramName.substring(bracketIndex) + currentSuffix };
} else if (dotIndex > 0) {
if (paramName.startsWith("this.")) {
int nextDotIndex = paramName.indexOf('.', 5);
if (nextDotIndex > 0) {
return new String[] { paramName.substring(0, nextDotIndex), paramName.substring(nextDotIndex) + currentSuffix };
}
return new String[] { paramName, currentSuffix };
}
return new String[] { paramName.substring(0, dotIndex), paramName.substring(dotIndex) + currentSuffix };
}
} }
return new String[] { paramName, currentSuffix }; return new String[] { paramName, currentSuffix };
} }
@@ -849,36 +1264,88 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return (TypeDeclaration) parent; return (TypeDeclaration) parent;
} }
protected Expression traceVariable(Expression expr) { protected Expression traceVariable(Expression expr) {
List<Expression> all = traceVariableAll(expr); List<Expression> all = traceVariableAll(expr, new HashSet<>());
return all.isEmpty() ? expr : all.get(all.size() - 1); return all.isEmpty() ? expr : all.get(all.size() - 1);
} }
protected List<Expression> traceVariableAll(Expression expr) { protected List<Expression> traceVariableAll(Expression expr) {
return traceVariableAll(expr, new HashSet<>());
}
protected List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
List<Expression> results = new ArrayList<>(); List<Expression> results = new ArrayList<>();
if (expr instanceof SimpleName sn) { if (expr instanceof SimpleName sn) {
String varName = sn.getIdentifier(); String varName = sn.getIdentifier();
if (!visitedVariables.add(varName)) {
return results; // Break infinite loop
}
MethodDeclaration enclosingMethod = findEnclosingMethod(expr); MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
if (enclosingMethod != null) { if (enclosingMethod != null) {
enclosingMethod.accept(new ASTVisitor() { enclosingMethod.accept(new ASTVisitor() {
@Override @Override
public boolean visit(VariableDeclarationFragment node) { public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
results.addAll(traceVariableAll(node.getInitializer())); results.addAll(traceVariableAll(node.getInitializer(), visitedVariables));
} }
return super.visit(node); return super.visit(node);
} }
@Override @Override
public boolean visit(Assignment node) { public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
results.addAll(traceVariableAll(node.getRightHandSide())); results.addAll(traceVariableAll(node.getRightHandSide(), visitedVariables));
} }
return super.visit(node); return super.visit(node);
} }
}); });
if (!results.isEmpty()) { if (!results.isEmpty()) {
visitedVariables.remove(varName);
return results; return results;
} }
// Tracing parameter callers (Inter-procedural within the same class)
for (Object paramObj : enclosingMethod.parameters()) {
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) paramObj;
if (param.getName().getIdentifier().equals(varName)) {
int paramIndex = enclosingMethod.parameters().indexOf(param);
TypeDeclaration enclosingType = findEnclosingType(enclosingMethod);
if (enclosingType != null) {
String mName = enclosingMethod.getName().getIdentifier();
List<Expression> callerExprs = new ArrayList<>();
enclosingType.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.MethodInvocation node) {
if (node.getName().getIdentifier().equals(mName) && node.arguments().size() > paramIndex) {
callerExprs.addAll(traceVariableAll((Expression) node.arguments().get(paramIndex), visitedVariables));
}
return super.visit(node);
}
@Override
public boolean visit(org.eclipse.jdt.core.dom.ExpressionMethodReference node) {
if (node.getName().getIdentifier().equals(mName)) {
if (node.getParent() instanceof org.eclipse.jdt.core.dom.MethodInvocation parentMi) {
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(parentMi)) {
callerExprs.addAll(traceVariableAll(parentMi.getExpression(), visitedVariables));
} else {
for (Object arg : parentMi.arguments()) {
if (arg != node) {
callerExprs.addAll(traceVariableAll((Expression) arg, visitedVariables));
}
}
}
}
}
return super.visit(node);
}
});
if (!callerExprs.isEmpty()) {
visitedVariables.remove(varName);
return callerExprs;
}
}
}
}
} }
visitedVariables.remove(varName);
} }
results.add(expr); results.add(expr);
return results; return results;

View File

@@ -45,7 +45,13 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
if (td2 != null) { if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier()); String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) { if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args)); List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
} }
} }
} else { } else {
@@ -58,7 +64,13 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} }
if (fallbackTypeFqn != null) { if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
List<String> impls = context.getImplementations(fallbackTypeFqn); List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) { for (String impl : impls) {
@@ -141,11 +153,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} }
} }
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...)) // Removed eager tracing here to allow dynamic tracing in resolveTriggerPointParameters
Expression tracedExpr = traceVariable(expr);
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
}
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) { if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
Expression firstArg = (Expression) cic.arguments().get(0); Expression firstArg = (Expression) cic.arguments().get(0);
@@ -161,6 +169,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} }
args.add(val); args.add(val);
} }
System.out.println("DEBUG resolveArguments: " + args);
return args; return args;
} }

View File

@@ -49,7 +49,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
if (td2 != null) { if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier()); String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) { if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args)); List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
} }
} }
} else { } else {
@@ -62,7 +68,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
} }
if (fallbackTypeFqn != null) { if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
List<String> impls = context.getImplementations(fallbackTypeFqn); List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) { for (String impl : impls) {

View File

@@ -36,4 +36,62 @@ public final class AstUtils {
return type.toString(); // fallback return type.toString(); // fallback
} }
} }
public static boolean isFunctionalCollectionMethod(org.eclipse.jdt.core.dom.MethodInvocation node) {
if (node.getExpression() == null) {
return false;
}
org.eclipse.jdt.core.dom.ITypeBinding typeBinding = node.getExpression().resolveTypeBinding();
if (typeBinding != null) {
if (isCollectionOrStreamType(typeBinding)) {
return true;
}
return false;
}
String methodName = node.getName().getIdentifier();
if (methodName.equals("forEach") || methodName.equals("map") ||
methodName.equals("filter") || methodName.equals("flatMap") ||
methodName.equals("anyMatch") || methodName.equals("allMatch") ||
methodName.equals("noneMatch") || methodName.equals("reduce") ||
methodName.equals("collect") || methodName.equals("removeIf") ||
methodName.equals("ifPresent") || methodName.equals("ifPresentOrElse") ||
methodName.equals("compute") || methodName.equals("computeIfAbsent") ||
methodName.equals("computeIfPresent") || methodName.equals("replaceAll") ||
methodName.equals("merge") || methodName.equals("peek") ||
methodName.equals("doOnNext") || methodName.equals("doOnSuccess") ||
methodName.equals("doOnError") || methodName.equals("concatMap") ||
methodName.equals("switchMap")) {
return true;
}
return false;
}
private static boolean isCollectionOrStreamType(org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
String fqn = typeBinding.getErasure().getQualifiedName();
if (fqn.startsWith("java.util.stream.") ||
fqn.equals("java.lang.Iterable") ||
fqn.equals("java.util.Collection") ||
fqn.equals("java.util.List") ||
fqn.equals("java.util.Set") ||
fqn.equals("java.util.Map") ||
fqn.equals("java.util.Iterator") ||
fqn.equals("java.util.Optional") ||
fqn.equals("reactor.core.publisher.Flux") ||
fqn.equals("reactor.core.publisher.Mono")) {
return true;
}
for (org.eclipse.jdt.core.dom.ITypeBinding intf : typeBinding.getInterfaces()) {
if (isCollectionOrStreamType(intf)) return true;
}
if (typeBinding.getSuperclass() != null) {
return isCollectionOrStreamType(typeBinding.getSuperclass());
}
return false;
}
} }

View File

@@ -65,7 +65,7 @@ public class CodebaseContext {
return Collections.unmodifiableList(libraryHints); return Collections.unmodifiableList(libraryHints);
} }
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/**", "**/node_modules/**", "**/out/**")); private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/classes/**", "**/build/libs/**", "**/build/tmp/**", "**/build/reports/**", "**/build/test-results/**", "**/node_modules/**", "**/out/**"));
private String[] classpath = new String[0]; private String[] classpath = new String[0];
private String[] sourcepath = new String[0]; private String[] sourcepath = new String[0];
private boolean resolveBindings = false; private boolean resolveBindings = false;
@@ -191,6 +191,8 @@ public class CodebaseContext {
indexType(td, packageName, javaFile); indexType(td, packageName, javaFile);
} else if (type instanceof EnumDeclaration ed) { } else if (type instanceof EnumDeclaration ed) {
indexEnum(ed, packageName, javaFile); indexEnum(ed, packageName, javaFile);
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration rd) {
indexRecord(rd, packageName, javaFile);
} }
} }
} }
@@ -235,6 +237,33 @@ public class CodebaseContext {
} }
} }
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;
// Treat as a class since it is a type
classes.put(fqn, (CompilationUnit) rd.getRoot());
classPaths.put(fqn, javaFile);
if (simpleNameToFqn.containsKey(simpleName)) {
String existingFqn = simpleNameToFqn.get(simpleName);
if (!existingFqn.equals(fqn)) {
ambiguousSimpleNames.add(simpleName);
}
} else {
simpleNameToFqn.put(simpleName, fqn);
}
// Recursively index nested types
for (Object type : rd.bodyDeclarations()) {
if (type instanceof TypeDeclaration nestedTd) {
indexType(nestedTd, fqn, javaFile);
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
indexRecord(nestedRd, fqn, javaFile);
}
}
}
public List<String> getImplementations(String interfaceName) { public List<String> getImplementations(String interfaceName) {
Set<String> allImpls = new HashSet<>(); Set<String> allImpls = new HashSet<>();
collectImplementations(interfaceName, allImpls, new HashSet<>()); collectImplementations(interfaceName, allImpls, new HashSet<>());

View File

@@ -0,0 +1,61 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class AnonymousClassTests {
@TempDir
Path tempDir;
@Test
void shouldResolveEventFromAnonymousClass() throws Exception {
String source = """
package com.example;
public class AnonymousService {
private EventService service;
public void process() {
MyEvent event = new MyEvent() {
@Override
public String getEvent() {
return "ANON_EVENT";
}
};
service.trigger(event.getEvent());
}
}
class EventService {
public void trigger(String ev) {}
}
class MyEvent {
public String getEvent() { return "BASE_EVENT"; }
}
""";
Files.writeString(tempDir.resolve("AnonymousService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("AnonymousService.java")
.sourceModule("com.example")
.event("event.getEvent()")
.lineNumber(12)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.AnonymousService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
assertThat(result.getPolymorphicEvents()).contains("ANON_EVENT");
}
}

View File

@@ -0,0 +1,54 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class CollectionIndexingTests {
@TempDir
Path tempDir;
@Test
void shouldResolveEventFromArrayAccess() throws Exception {
String source = """
package com.example;
public class ArrayService {
private EventService service;
public void process() {
String[] events = new String[] {"EVENT_1", "EVENT_2"};
service.trigger(events[0]);
}
}
class EventService {
public void trigger(String ev) {}
}
""";
Files.writeString(tempDir.resolve("ArrayService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("ArrayService.java")
.sourceModule("com.example")
.event("events[0]")
.lineNumber(7)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.ArrayService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
// When using array indexing, we should ideally extract the indexed element or at least all elements in the array
assertThat(result.getPolymorphicEvents()).contains("EVENT_1");
}
}

View File

@@ -1097,7 +1097,7 @@ class HeuristicCallGraphEngineTest {
} }
@Test @Test
void shouldTraceLastTextualAssignmentInTryCatchBlocks() throws IOException { void shouldTraceAllTextualAssignmentsInTryCatchBlocks() throws IOException {
String source = """ String source = """
package com.example; package com.example;
public class OrderController { public class OrderController {
@@ -1142,9 +1142,69 @@ class HeuristicCallGraphEngineTest {
assertThat(chains).hasSize(1); assertThat(chains).hasSize(1);
CallChain chain = chains.get(0); CallChain chain = chains.get(0);
// The ASTVisitor picks up Assignments in textual order.
// CANCELLED is the last one textually in the method. assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED");
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED"); }
@Test
void shouldTraceEventsThroughMapStructInterfaceImpl() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
private OrderMapper mapper = new OrderMapperImpl();
public void processOrderEvent(String eventDtoStr) {
OrderEvent e = mapper.toEvent(eventDtoStr);
service.updateOrderState(e.getEvent());
}
}
class OrderService {
public void updateOrderState(String event) {}
}
interface OrderMapper {
OrderEvent toEvent(String source);
}
class OrderMapperImpl implements OrderMapper {
@Override
public OrderEvent toEvent(String source) {
return new OrderEvent("MAPPED_EVENT");
}
}
class OrderEvent {
private String event;
public OrderEvent(String event) { this.event = event; }
public String getEvent() { return event; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_test_mapstruct");
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("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("MAPPED_EVENT");
} }
@Test @Test
@@ -1190,7 +1250,7 @@ class HeuristicCallGraphEngineTest {
CallChain chain = chains.get(0); CallChain chain = chains.get(0);
// We don't dynamically resolve array indexes, but it shouldn't crash. // We don't dynamically resolve array indexes, but it shouldn't crash.
// It returns the array access expression as a string. // It returns the array access expression as a string.
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("events[0]"); assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("new OrderEvents[]{OrderEvents.PAY}[0]");
} }
@Test @Test
@@ -2134,5 +2194,563 @@ class HeuristicCallGraphEngineTest {
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST"); 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 shouldResolveEventFromArrayAndList() throws IOException {
String source = """
package com.example;
import java.util.List;
public class OrderController {
private OrderService service;
public void handleArray() {
String[] events = {"ARRAY_EVENT"};
service.process(events[0]);
}
public void handleList() {
List<String> listEvents = List.of("LIST_EVENT");
service.process(listEvents.get(0));
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_array");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleArray").build();
EntryPoint entryPoint2 = 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(entryPoint1, entryPoint2), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("ARRAY_EVENT", "LIST_EVENT");
}
@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");
}
@Test
void shouldResolveEventFromBranchingAssignments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(boolean isRefund) {
String event;
if (isRefund) {
event = "REFUND_EVENT";
} else {
event = "CHARGE_EVENT";
}
service.process(event);
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_branch");
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);
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("REFUND_EVENT", "CHARGE_EVENT");
}
@Test
void shouldResolveEventFromLombokDataAndAllArgsConstructor() throws IOException {
String source = """
package com.example;
import lombok.Data;
import lombok.AllArgsConstructor;
public class OrderController {
private OrderService service;
public void handleLombok() {
LombokEvent e = new LombokEvent("LOMBOK_EVENT", 1);
service.process(e.getEvent());
}
}
class OrderService {
public void process(String event) {}
}
@Data
@AllArgsConstructor
class LombokEvent {
private String event;
private int id;
}
""";
Path tempDir = Files.createTempDirectory("callgraph_lombok");
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("handleLombok").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("LOMBOK_EVENT");
}
@Test
void shouldResolveEventFromMethodReference() throws IOException {
String source = """
package com.example;
import java.util.List;
public class OrderController {
private OrderService service;
public void handleMethodRef() {
List<RefEvent> events = List.of(new RefEvent("METHOD_REF_EVENT"));
events.forEach(this::delegateProcessing);
}
private void delegateProcessing(RefEvent e) {
service.process(e.getType());
}
}
class OrderService {
public void process(String event) {}
}
class RefEvent {
private String type;
public RefEvent(String type) { this.type = type; }
public String getType() { return type; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_method_ref");
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("handleMethodRef").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("METHOD_REF_EVENT");
}
@Test
void shouldResolveEventFromJavaRecord() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleRecord() {
RecordEvent e = new RecordEvent("RECORD_EVENT", 42);
service.process(e.type());
}
}
class OrderService {
public void process(String event) {}
}
record RecordEvent(String type, int someId) {}
""";
Path tempDir = Files.createTempDirectory("callgraph_record");
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("handleRecord").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("RECORD_EVENT");
}
@Test
void shouldFailHeuristicAndRequireAccurateLombokMapping() throws IOException {
String source = """
package com.example;
import lombok.Data;
import lombok.AllArgsConstructor;
public class OrderController {
private OrderService service;
public void handleLombok() {
MultiEvent e = new MultiEvent("CORRECT_EVENT", "WRONG_EVENT");
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
@Data
@AllArgsConstructor
class MultiEvent {
private String eventType;
private String unrelatedString;
}
""";
Path tempDir = Files.createTempDirectory("callgraph_lombok_heuristic_break");
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("handleLombok").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())
.containsExactly("CORRECT_EVENT"); // If heuristic rips both, it will fail because it contains WRONG_EVENT too!
}
@Test
void shouldNotExtractConstantsFromNestedClassInstanceCreations() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleNestedConstructors() {
// The event contains a nested constructor. The heuristic should NOT extract WRONG_EVENT.
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload("WRONG_EVENT"));
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
class OuterEvent {
private String eventType;
private InnerPayload payload;
public OuterEvent(String eventType, InnerPayload payload) {
this.eventType = eventType;
this.payload = payload;
}
public String getEventType() { return eventType; }
}
class InnerPayload {
private String someData;
public InnerPayload(String someData) { this.someData = someData; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_constructor");
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("handleNestedConstructors").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())
.containsExactly("CORRECT_EVENT");
}
@Test
void shouldNotExtractConstantsFromNestedArraysOfObjects() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleNestedArrays() {
// The event contains a nested array of objects. The heuristic should NOT extract WRONG_EVENT.
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload[] { new InnerPayload("WRONG_EVENT") });
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
class OuterEvent {
private String eventType;
private InnerPayload[] payloads;
public OuterEvent(String eventType, InnerPayload[] payloads) {
this.eventType = eventType;
this.payloads = payloads;
}
public String getEventType() { return eventType; }
}
class InnerPayload {
private String someData;
public InnerPayload(String someData) { this.someData = someData; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_array");
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("handleNestedArrays").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())
.containsExactly("CORRECT_EVENT");
}
@Test
void shouldResolveEnumValueOfFromString(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class Endpoint {
private OrderService service;
public void trigger(String eventString) {
service.doSomething(eventString);
}
}
class OrderService {
public void doSomething(String str) {
MyEvents event = mapToEnum(str);
sendEvent(event);
}
private MyEvents mapToEnum(String str) {
return MyEvents.valueOf(str);
}
public void sendEvent(MyEvents e) {}
}
enum MyEvents { A, B }
""";
Files.writeString(tempDir.resolve("Endpoint.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.Endpoint")
.methodName("trigger")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("sendEvent")
.event("e")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("MyEvents.A", "MyEvents.B");
}
} }

View File

@@ -0,0 +1,66 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class StreamApiTests {
@TempDir
Path tempDir;
@Test
void shouldHandleStreamApiChains() throws Exception {
String source = """
package com.example;
import java.util.List;
public class StreamService {
private EventService service;
public void process(List<MyEvent> events) {
events.stream()
.filter(e -> e.isValid())
.map(e -> e.toEvent())
.forEach(t -> service.trigger(t.getEvent()));
}
}
class EventService {
public void trigger(String ev) {}
}
class MyEvent {
public boolean isValid() { return true; }
public OrderEvent toEvent() { return new OrderEvent(); }
}
class OrderEvent {
public String getEvent() { return "STREAM_EVENT"; }
}
""";
Files.writeString(tempDir.resolve("StreamService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("StreamService.java")
.sourceModule("com.example")
.event("t.getEvent()")
.lineNumber(11)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.StreamService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
System.out.println("TEST RESULT: " + result);
System.out.println("POLY EVENTS: " + result.getPolymorphicEvents());
assertThat(result.getPolymorphicEvents()).isNotNull().containsExactlyInAnyOrder("STREAM_EVENT");
}
}

View File

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

View File

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