This commit is contained in:
2026-06-21 17:53:26 +02:00
parent 23d79d1930
commit c903b079d3
6 changed files with 2661 additions and 2107 deletions

View File

@@ -14,684 +14,17 @@ import java.util.*;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
@Slf4j
public class JdtCallGraphEngine implements CallGraphEngine {
private final CodebaseContext context;
private final ConstantResolver constantResolver;
public class JdtCallGraphEngine extends AbstractCallGraphEngine {
private final InjectionPointAnalyzer injectionAnalyzer;
private String currentMethodFqn;
private Map<String, List<CallEdge>> graph;
public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) {
this.context = context;
this.constantResolver = new ConstantResolver();
super(context);
this.injectionAnalyzer = injectionAnalyzer;
}
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
Map<String, List<CallEdge>> callGraph = buildCallGraph();
List<CallChain> chains = new ArrayList<>();
for (EntryPoint ep : entryPoints) {
String startMethod = ep.getClassName() + "." + ep.getMethodName();
boolean foundAny = false;
for (TriggerPoint tp : triggers) {
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
if (path != null) {
foundAny = true;
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
String contextMachineId = extractContextMachineId(path, callGraph);
chains.add(CallChain.builder()
.entryPoint(ep)
.triggerPoint(resolvedTp)
.methodChain(path)
.contextMachineId(contextMachineId)
.build());
}
}
if (!foundAny && log.isDebugEnabled()) {
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
}
}
return chains;
}
private TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
if (path.size() < 2) return tp;
String event = tp.getEvent();
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
String currentParamName = event;
String resolvedValue = event;
String methodSuffix = "";
// Extract method calls like .getType() so we can trace the base parameter
int dotIndex = currentParamName.indexOf('.');
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) {
char nextChar = currentParamName.charAt(dotIndex + 1);
if (Character.isLowerCase(nextChar)) {
methodSuffix = currentParamName.substring(dotIndex);
currentParamName = currentParamName.substring(0, dotIndex);
}
}
// Walk backwards up the call chain
for (int i = path.size() - 1; i > 0; i--) {
String target = path.get(i);
String caller = path.get(i - 1);
// Find parameter index in target method
int paramIndex = getParameterIndex(target, currentParamName);
if (paramIndex < 0) {
// Not a parameter. Maybe it's a local variable initialized from a parameter or method call?
String tracedVar = traceLocalVariable(target, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
// Extract method calls like .getType() from the traced variable
int dotIdx = tracedVar.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
paramIndex = getParameterIndex(target, currentParamName);
}
}
if (paramIndex < 0) {
break; // Parameter name changed or not found, stop tracing
}
// Find the edge from caller to target to get the argument passed
List<CallEdge> edges = callGraph.get(caller);
boolean found = false;
if (edges != null) {
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
if (paramIndex < edge.getArguments().size()) {
String arg = edge.getArguments().get(paramIndex);
if (arg != null) {
// If the argument passed has a method call, extract it
int dotIdx = arg.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) {
methodSuffix = arg.substring(dotIdx) + methodSuffix;
arg = arg.substring(0, dotIdx);
}
currentParamName = arg;
resolvedValue = arg + methodSuffix;
found = true;
break;
}
}
}
}
}
if (!found) break; // Could not map argument
}
// Final check on the entry method
String entryMethod = path.get(0);
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
if (entryParamIndex < 0) {
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
int dotIdx = tracedVar.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
}
}
List<String> polymorphicEvents = new ArrayList<>();
// 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());
exprParser.setSource(resolvedValue.toCharArray());
exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null);
String varName = null;
String methodName = null;
String declaredType = null;
String sourceMethod = null;
if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
methodName = mi.getName().getIdentifier();
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
declaredType = ce.getType().toString();
sourceMethod = "inline-cast";
} else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
} else {
// Fallback for complex chained expressions
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "";
if (!exprStr.contains("(")) {
varName = exprStr;
}
}
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType); // Track the payload type as the event
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ConditionalExpression cond) {
if (cond.getThenExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicThen) {
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicThen.getType()));
}
if (cond.getElseExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicElse) {
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicElse.getType()));
}
sourceMethod = "inline-ternary";
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
} else if (exprNode instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
methodName = "VariableReference"; // We just want to trigger deep trace
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
} else if (exprNode instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
}
if (methodName != null) {
if (varName != null && declaredType == null) {
for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
sourceMethod = methodFqn;
break;
}
}
// If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent())
if (declaredType == null && varName.matches("^[A-Z].*")) {
org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName);
if (staticTd != null) {
declaredType = context.getFqn(staticTd);
sourceMethod = "static-call";
}
}
}
if (declaredType != null) {
System.out.println("DEEP TRACE: " + sourceMethod + " " + (varName != null ? varName : "inline") + " -> " + declaredType);
List<String> typesToInspect = new ArrayList<>();
if ("inline-instantiation".equals(sourceMethod)) {
typesToInspect.add(declaredType);
} else {
typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType));
}
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
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);
System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants);
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;
for (String ev : polymorphicEvents) {
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
if (val.equals(val.toUpperCase()) && val.length() > 2) {
hasValidConstant = true;
break;
}
}
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);
}
}
// 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;
});
}
List<String> newPolyEvents = new ArrayList<>();
for (String pe : polymorphicEvents) {
List<String> resolved = resolveClassConstantReturns(pe, context, null);
if (resolved != null && !resolved.isEmpty()) {
newPolyEvents.addAll(resolved);
} else {
newPolyEvents.add(pe);
}
}
polymorphicEvents = newPolyEvents;
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(polymorphicEvents)
.build();
}
return tp;
}
private int getParameterIndex(String methodFqn, String paramName) {
if (methodFqn == null || !methodFqn.contains(".")) return -1;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
for (int i = 0; i < md.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
if (svd.getName().getIdentifier().equals(paramName)) {
return i;
}
}
}
}
return -1;
}
private String getVariableDeclaredType(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
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)) {
return svd.getType().toString();
}
}
final String[] foundType = new String[1];
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType().toString();
}
}
return super.visit(node);
}
});
}
return foundType[0];
}
}
return null;
}
private List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
if (depth > 20) return Collections.emptyList();
String fqn = className + "." + methodName;
if (!visited.add(fqn)) return Collections.emptyList();
List<String> constants = new ArrayList<>();
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
if (tempTd == null) {
tempTd = context.getTypeDeclaration(className);
}
final TypeDeclaration td = tempTd;
if (td == null) {
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
}
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
Expression retExpr = node.getExpression();
if (retExpr != null) {
boolean handled = false;
if (retExpr instanceof MethodInvocation mi) {
// Follow delegation first
String called = resolveCalledMethod(mi);
System.out.println("DEEP TRACE RESOLVED CALLED: " + called);
if (called != null && called.contains(".")) {
if (visited.contains(called)) {
handled = true;
} else {
String cName = called.substring(0, called.lastIndexOf('.'));
String mName = called.substring(called.lastIndexOf('.') + 1);
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
} else if (retExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
String called = superFqn + "." + smi.getName().getIdentifier();
if (visited.contains(called)) {
handled = true;
} else {
String cName = superFqn;
String mName = smi.getName().getIdentifier();
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
}
if (!handled) {
extractConstantsFromExpression(retExpr, constants);
String val = constantResolver.resolve(retExpr, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
String parsed = eVal.substring(eVal.lastIndexOf('.') + 1);
if (!constants.contains(parsed)) constants.add(parsed);
}
} else {
if (!constants.contains(val)) constants.add(val);
}
} else if (retExpr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) {
extractConstantsFromExpression(ce, constants);
} else if (retExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
constants.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
} else if (retExpr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (retExpr instanceof SimpleName sn) {
List<String> consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
if (!consts.isEmpty()) {
constants.addAll(consts);
} else {
constants.add(sn.toString());
}
} else if (retExpr instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
List<String> consts = traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited);
if (!consts.isEmpty()) {
constants.addAll(consts);
} else {
constants.add(fa.getName().getIdentifier());
}
}
}
}
return super.visit(node);
}
});
}
}
visited.remove(fqn);
return constants;
}
private List<String> resolveClassConstantReturns(String className, click.kamil.springstatemachineexporter.ast.common.CodebaseContext context, CompilationUnit contextCu) {
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
if (td == null) td = context.getTypeDeclaration(className);
if (td == null) return null;
final List<String> resolvedConstants = new ArrayList<>();
for (MethodDeclaration md : td.getMethods()) {
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
extractConstantsFromExpression(node.getExpression(), resolvedConstants);
return super.visit(node);
}
});
}
if (!resolvedConstants.isEmpty()) return resolvedConstants;
}
return null;
}
private void extractConstantsFromExpression(org.eclipse.jdt.core.dom.Expression expr, List<String> constants) {
if (expr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) {
constants.add(sl.getLiteralValue());
} else if (expr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) {
extractConstantsFromExpression(ce.getThenExpression(), constants);
extractConstantsFromExpression(ce.getElseExpression(), constants);
} else if (expr instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe) {
extractConstantsFromExpression(pe.getExpression(), constants);
} else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) {
extractConstantsFromExpression(ce.getExpression(), constants);
} else if (expr instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
se.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.YieldStatement ys) {
extractConstantsFromExpression(ys.getExpression(), constants);
return super.visit(ys);
}
@Override
public boolean visit(org.eclipse.jdt.core.dom.ExpressionStatement es) {
extractConstantsFromExpression(es.getExpression(), constants);
return super.visit(es);
}
@Override
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
extractConstantsFromExpression(rs.getExpression(), constants);
return super.visit(rs);
}
});
} else if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
String methodName = mi.getName().getIdentifier();
// 1. Local setter tracking
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 propName = methodName.startsWith("get") ? methodName.substring(3) : methodName;
org.eclipse.jdt.core.dom.Block block = findEnclosingBlock(mi);
if (block != null) {
for (Object stmtObj : block.statements()) {
if (stmtObj == mi.getParent() || stmtObj == mi) break;
if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
if (es.getExpression() instanceof org.eclipse.jdt.core.dom.MethodInvocation setterMi) {
if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) {
if (setterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName setterSn && setterSn.getIdentifier().equals(varName)) {
if (!setterMi.arguments().isEmpty()) {
extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) setterMi.arguments().get(0), constants);
return;
}
}
}
} else if (es.getExpression() instanceof org.eclipse.jdt.core.dom.Assignment assignment) {
if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName faSn && faSn.getIdentifier().equals(varName)) {
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
return;
}
}
} else if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.QualifiedName qqn) {
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
return;
}
}
}
}
}
}
}
}
// 2. 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);
if (values != null) constants.addAll(values);
}
}
}
private org.eclipse.jdt.core.dom.Block findEnclosingBlock(org.eclipse.jdt.core.dom.ASTNode node) {
org.eclipse.jdt.core.dom.ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof org.eclipse.jdt.core.dom.Block)) {
parent = parent.getParent();
}
return (org.eclipse.jdt.core.dom.Block) parent;
}
private String traceLocalVariable(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
final Expression[] initializer = new Expression[1];
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializer[0] = node.getInitializer();
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
initializer[0] = node.getRightHandSide();
}
return super.visit(node);
}
});
if (initializer[0] != null) {
Expression expr = traceVariable(initializer[0]);
if (expr instanceof MethodInvocation mi) {
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
Expression innerMost = unwrapMethodInvocation(mi, 0);
if (innerMost instanceof MethodInvocation innerMi) {
if (innerMi.getExpression() instanceof SimpleName sn) {
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
}
return innerMi.getName().getIdentifier() + "()";
}
if (innerMost instanceof SimpleName sn) {
return sn.getIdentifier();
}
return innerMost.toString();
}
if (expr instanceof SimpleName sn) {
return sn.getIdentifier();
}
return expr.toString();
}
}
}
return null;
}
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
if (depth > 5) return mi;
if (!mi.arguments().isEmpty()) {
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof MethodInvocation innerMi) {
return unwrapMethodInvocation(innerMi, depth + 1);
}
return arg;
}
return mi;
}
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
for (String node : path) {
List<CallEdge> edges = callGraph.get(node);
if (edges != null) {
for (CallEdge edge : edges) {
String target = edge.getTargetMethod();
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
// Persister signatures usually like: restore(stateMachine, contextObj)
if (edge.getArguments().size() >= 2) {
return edge.getArguments().get(1); // The contextObj / machineId
}
}
}
}
}
return null;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
return (MethodDeclaration) parent;
}
private Map<String, List<CallEdge>> buildCallGraph() {
protected Map<String, List<CallEdge>> buildCallGraph() {
graph = new HashMap<>();
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(new ASTVisitor() {
@@ -855,7 +188,7 @@ public class JdtCallGraphEngine implements CallGraphEngine {
return allResolved;
}
private String resolveCalledMethod(MethodInvocation node) {
protected String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier();
@@ -1024,295 +357,4 @@ public class JdtCallGraphEngine implements CallGraphEngine {
return null;
}
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
if (start.equals(target)) return new ArrayList<>(List.of(start));
if (!visited.add(start)) return null; // Path-scoped cycle detection
List<CallEdge> neighbors = graph.get(start);
if (neighbors != null) {
for (CallEdge edge : neighbors) {
String neighbor = edge.getTargetMethod();
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
visited.remove(start);
return new ArrayList<>(List.of(start, target));
}
List<String> path = findPath(neighbor, target, graph, visited);
if (path != null) {
path.add(0, start);
visited.remove(start);
return path;
}
}
}
if (log.isDebugEnabled()) {
log.debug("Path search dead-end at {} when looking for {}", start, target);
}
visited.remove(start);
return null;
}
private boolean isHeuristicMatch(String neighbor, String target) {
if (neighbor.equals(target)) return true;
if (target.endsWith("." + neighbor)) return true;
if (neighbor.endsWith("." + target)) return true;
// If both are fully qualified (contain a dot) and didn't match above, they are definitely different methods
if (neighbor.contains(".") && target.contains(".")) {
return false;
}
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
return simpleNeighbor.equals(simpleTarget);
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
private Expression traceVariable(Expression expr) {
if (expr instanceof SimpleName sn) {
String varName = sn.getIdentifier();
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
if (enclosingMethod != null) {
final Expression[] initializer = new Expression[1];
enclosingMethod.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializer[0] = node.getInitializer();
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
initializer[0] = node.getRightHandSide();
}
return super.visit(node);
}
});
if (initializer[0] != null) {
return traceVariable(initializer[0]);
}
}
}
return expr;
}
private List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
List<String> results = new ArrayList<>();
// 1. Check Field Declarations
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(fieldName) && frag.getInitializer() != null) {
Expression right = frag.getInitializer();
if (right instanceof MethodInvocation mi) {
String calledName = mi.getName().getIdentifier();
String targetClass = context.getFqn(td);
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
} else {
String val = constantResolver.resolve(right, context);
if (val != null) results.add(val);
}
}
}
}
org.eclipse.jdt.core.dom.ASTVisitor assignmentVisitor = new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(Assignment node) {
Expression left = node.getLeftHandSide();
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
Expression right = node.getRightHandSide();
if (right instanceof MethodInvocation mi) {
String calledName = mi.getName().getIdentifier();
String targetClass = context.getFqn(td);
if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
} else if (mi.getExpression() instanceof SimpleName) {
String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
if (targetTd != null) {
targetClass = context.getFqn(targetTd);
}
}
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
} else {
String val = constantResolver.resolve(right, context);
if (val != null) results.add(val);
}
}
return super.visit(node);
}
@Override
public boolean visit(ConstructorInvocation node) {
processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited);
return super.visit(node);
}
@Override
public boolean visit(SuperConstructorInvocation node) {
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
String superFqn = context.getSuperclassFqn(enclosingTd);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited);
}
}
return super.visit(node);
}
};
// 2. Check Constructors
for (MethodDeclaration md : td.getMethods()) {
if (md.isConstructor() && md.getBody() != null) {
md.getBody().accept(assignmentVisitor);
}
}
// 3. Check Initializers
for (Object bodyDecl : td.bodyDeclarations()) {
if (bodyDecl instanceof org.eclipse.jdt.core.dom.Initializer init && init.getBody() != null) {
init.getBody().accept(assignmentVisitor);
}
}
return results;
}
private void processConstructorInvocationArgs(List<?> arguments, TypeDeclaration targetTd, ASTNode callNode, List<String> results, String fieldName, CodebaseContext context, Set<String> visited) {
if (targetTd == null) return;
MethodDeclaration callerMd = findEnclosingMethod(callNode);
org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = null;
if (callNode instanceof ConstructorInvocation ci) {
resolvedBinding = ci.resolveConstructorBinding();
} else if (callNode instanceof SuperConstructorInvocation sci) {
resolvedBinding = sci.resolveConstructorBinding();
}
for (MethodDeclaration otherMd : targetTd.getMethods()) {
boolean matches = false;
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
} else {
matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd;
}
if (matches) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new java.util.HashSet<>());
if (targetIdx >= 0 && targetIdx < arguments.size()) {
Expression arg = (Expression) arguments.get(targetIdx);
String val = constantResolver.resolve(arg, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
results.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
results.add(val);
}
}
}
}
}
}
private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, java.util.Set<MethodDeclaration> visited) {
if (constructorMd == null || constructorMd.getBody() == null) return -1;
if (!visited.add(constructorMd)) return -1;
final int[] foundIdx = {-1};
constructorMd.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment asn) {
Expression left = asn.getLeftHandSide();
if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) ||
(left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName))) {
Expression right = asn.getRightHandSide();
if (right instanceof SimpleName snRight) {
String rightName = snRight.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(rightName)) {
foundIdx[0] = i;
}
}
}
}
return super.visit(asn);
}
@Override
public boolean visit(ConstructorInvocation node) {
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
for (MethodDeclaration otherMd : enclosingTd.getMethods()) {
if (otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size()) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
if (arg instanceof SimpleName snArg) {
String argName = snArg.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperConstructorInvocation node) {
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
String superFqn = context.getSuperclassFqn(enclosingTd);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
for (MethodDeclaration otherMd : superTd.getMethods()) {
if (otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size()) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
if (arg instanceof SimpleName snArg) {
String argName = snArg.getIdentifier();
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
}
}
}
return super.visit(node);
}
});
return foundIdx[0];
}
}