7 Commits

Author SHA1 Message Date
5a78110dd9 refactor: implement phase 1 - unify parameter tracing in base class 2026-06-24 23:43:35 +02:00
ac2afca053 heuristics updated 2026-06-24 22:04:15 +02:00
c6db825807 test added 2026-06-24 22:03:57 +02:00
1b1e036680 te21 2026-06-24 18:25:36 +02:00
ae75379f77 te2 2026-06-24 18:03:19 +02:00
e2f8be9c6b te 2026-06-24 16:37:57 +02:00
cb97392bdd test 2026-06-24 06:57:46 +02:00
7 changed files with 1301 additions and 143 deletions

View File

@@ -1,11 +1,20 @@
package click.kamil.springstatemachineexporter.analysis.model; package click.kamil.springstatemachineexporter.analysis.model;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.List; import java.util.List;
@Data @Data
@NoArgsConstructor
@AllArgsConstructor
public class CallEdge { public class CallEdge {
private final String targetMethod; private String targetMethod;
private final List<String> arguments; private List<String> arguments;
private String receiver;
public CallEdge(String targetMethod, List<String> arguments) {
this(targetMethod, arguments, null);
}
} }

View File

@@ -14,6 +14,14 @@ public class ConstantResolver {
return resolveInternal(expr, context, new HashSet<>()); return resolveInternal(expr, context, new HashSet<>());
} }
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context) {
return evaluateSwitchExpression(se, paramValues, context, new HashSet<>());
}
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context) {
return evaluateSwitchStatement(ss, paramValues, context, new HashSet<>());
}
private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) { private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) {
if (expr == null) return null; if (expr == null) return null;

View File

@@ -21,6 +21,99 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
this.constantResolver = new ConstantResolver(); this.constantResolver = new ConstantResolver();
} }
@FunctionalInterface
protected interface RhsResolver {
String resolve(Expression rhs, Map<String, String> paramValues, TypeDeclaration td);
}
protected Map<String, String> buildParameterValuesMap(String caller, String target,
Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
Map<String, String> paramValues = new HashMap<>();
List<CallEdge> edges = callGraph.get(caller);
if (edges == null) {
return paramValues;
}
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
List<String> args = edge.getArguments();
if (args == null || args.isEmpty()) break;
int lastDot = target.lastIndexOf('.');
if (lastDot < 0) break;
String className = target.substring(0, lastDot);
String methodName = target.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) break;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null) break;
List<String> paramNames = new ArrayList<>();
for (Object paramObj : md.parameters()) {
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
paramNames.add(param.getName().getIdentifier());
}
int minSize = Math.min(paramNames.size(), args.size());
for (int j = 0; j < minSize; j++) {
String argValue = args.get(j);
String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph);
paramValues.put(paramNames.get(j), resolvedArgValue);
}
break;
}
}
return paramValues;
}
protected String resolveArgumentValue(String argValue, String callerMethod, List<String> path, int pathIndex, Map<String, List<CallEdge>> callGraph) {
if (argValue == null) return null;
if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*")) {
return argValue;
}
int callerIndex = path.indexOf(callerMethod);
if (callerIndex <= 0) return argValue;
String prevCaller = path.get(callerIndex - 1);
List<CallEdge> prevEdges = callGraph.get(prevCaller);
if (prevEdges == null) return argValue;
for (CallEdge edge : prevEdges) {
if (edge.getTargetMethod().equals(callerMethod) || isHeuristicMatch(edge.getTargetMethod(), callerMethod)) {
List<String> prevArgs = edge.getArguments();
if (prevArgs == null || prevArgs.isEmpty()) break;
int lastDot = callerMethod.lastIndexOf('.');
if (lastDot < 0) break;
String className = callerMethod.substring(0, lastDot);
String methodName = callerMethod.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) break;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null) break;
List<String> paramNames = new ArrayList<>();
for (Object paramObj : md.parameters()) {
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
paramNames.add(param.getName().getIdentifier());
}
int paramIdx = paramNames.indexOf(argValue);
if (paramIdx >= 0 && paramIdx < prevArgs.size()) {
String prevArg = prevArgs.get(paramIdx);
return resolveArgumentValue(prevArg, prevCaller, path, pathIndex, callGraph);
}
break;
}
}
return argValue;
}
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) { public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
Map<String, List<CallEdge>> callGraph = buildCallGraph(); Map<String, List<CallEdge>> callGraph = buildCallGraph();
List<CallChain> chains = new ArrayList<>(); List<CallChain> chains = new ArrayList<>();
@@ -986,13 +1079,48 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
* field assignments found in the matching constructor body. * field assignments found in the matching constructor body.
* *
* <p>Handles both {@code this.field = param} and bare {@code field = param} assignment forms. * <p>Handles both {@code this.field = param} and bare {@code field = param} assignment forms.
*
* @param rhsResolver optional strategy to resolve non-SimpleName RHS expressions (e.g., method calls)
*/
protected java.util.Map<String, String> buildFieldValuesFromCIC(
org.eclipse.jdt.core.dom.TypeDeclaration td,
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
CodebaseContext context,
RhsResolver rhsResolver) {
// Use the CIC's actual type as the starting point, not the declared type
String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName);
if (actualTd == null) {
actualTd = td; // Fallback to passed-in type
}
java.util.Map<String, String> fieldValues = new java.util.HashMap<>();
java.util.Set<String> visitedCtors = new java.util.HashSet<>();
buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors, rhsResolver);
return fieldValues;
}
/**
* Backward-compatible overload without RhsResolver.
*/ */
protected java.util.Map<String, String> buildFieldValuesFromCIC( protected java.util.Map<String, String> buildFieldValuesFromCIC(
org.eclipse.jdt.core.dom.TypeDeclaration td, org.eclipse.jdt.core.dom.TypeDeclaration td,
org.eclipse.jdt.core.dom.ClassInstanceCreation cic, org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
CodebaseContext context) { CodebaseContext context) {
return buildFieldValuesFromCIC(td, cic, context, null);
}
java.util.Map<String, String> fieldValues = new java.util.HashMap<>(); private void buildFieldValuesFromCICRecursive(
org.eclipse.jdt.core.dom.TypeDeclaration td,
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
CodebaseContext context,
java.util.Map<String, String> fieldValues,
java.util.Set<String> visitedCtors,
RhsResolver rhsResolver) {
String tdKey = context.getFqn(td);
if (!visitedCtors.add(tdKey)) return;
for (org.eclipse.jdt.core.dom.MethodDeclaration ctor : td.getMethods()) { for (org.eclipse.jdt.core.dom.MethodDeclaration ctor : td.getMethods()) {
if (!ctor.isConstructor() || ctor.getBody() == null) continue; if (!ctor.isConstructor() || ctor.getBody() == null) continue;
@@ -1029,12 +1157,17 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
fieldName = fa.getName().getIdentifier(); fieldName = fa.getName().getIdentifier();
} else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn } else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn
&& !paramValues.containsKey(sn.getIdentifier())) { && !paramValues.containsKey(sn.getIdentifier())) {
// Bare field assignment (not a parameter-to-itself assignment)
fieldName = sn.getIdentifier(); fieldName = sn.getIdentifier();
} }
if (fieldName != null && rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) { if (fieldName != null) {
String paramVal = paramValues.get(snRhs.getIdentifier()); String paramVal = null;
if (rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
paramVal = paramValues.get(snRhs.getIdentifier());
} else if (rhsResolver != null) {
paramVal = rhsResolver.resolve(rhs, paramValues, td);
}
if (paramVal != null) { if (paramVal != null) {
fieldValues.put(fieldName, paramVal); fieldValues.put(fieldName, paramVal);
fieldValues.put("this." + fieldName, paramVal); fieldValues.put("this." + fieldName, paramVal);
@@ -1043,9 +1176,152 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return super.visit(node); return super.visit(node);
} }
}); });
// 3. Follow super constructor chain
ctor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.SuperConstructorInvocation sci) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
processSuperConstructorArgs(superTd, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
}
}
return super.visit(sci);
}
});
break; // first matching constructor is enough break; // first matching constructor is enough
} }
return fieldValues; }
private void processSuperConstructorArgs(
TypeDeclaration superTd,
List<?> superArgs,
java.util.Map<String, String> subClassParamValues,
CodebaseContext context,
java.util.Map<String, String> fieldValues,
java.util.Set<String> visitedCtors,
RhsResolver rhsResolver) {
if (superTd == null) return;
for (MethodDeclaration superCtor : superTd.getMethods()) {
if (!superCtor.isConstructor() || superCtor.getBody() == null) continue;
if (superCtor.parameters().size() != superArgs.size()) continue;
// Map super() argument values to superclass constructor parameters
// superArgs may be parameter references from the subclass constructor
java.util.Map<String, String> superParamValues = new java.util.HashMap<>();
for (int i = 0; i < superCtor.parameters().size(); i++) {
String pName = ((org.eclipse.jdt.core.dom.SingleVariableDeclaration)
superCtor.parameters().get(i)).getName().getIdentifier();
org.eclipse.jdt.core.dom.Expression argExpr =
(org.eclipse.jdt.core.dom.Expression) superArgs.get(i);
// If the argument is a SimpleName (parameter reference), resolve it using subclass paramValues
String resolved = null;
if (argExpr instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
resolved = subClassParamValues.get(sn.getIdentifier());
}
if (resolved == null) {
java.util.List<String> consts = new java.util.ArrayList<>();
extractConstantsFromArgument(argExpr, consts);
if (!consts.isEmpty()) {
resolved = consts.get(0);
} else {
resolved = constantResolver.resolve(argExpr, context);
}
}
if (resolved != null) {
superParamValues.put(pName, resolved);
}
}
if (superParamValues.isEmpty()) continue;
// Scan superclass constructor body for field assignments
superCtor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.Assignment node) {
org.eclipse.jdt.core.dom.Expression lhs = node.getLeftHandSide();
org.eclipse.jdt.core.dom.Expression rhs = node.getRightHandSide();
String fieldName = null;
if (lhs instanceof org.eclipse.jdt.core.dom.FieldAccess fa
&& fa.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression) {
fieldName = fa.getName().getIdentifier();
} else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn
&& !superParamValues.containsKey(sn.getIdentifier())) {
fieldName = sn.getIdentifier();
}
if (fieldName != null) {
String paramVal = null;
if (rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
paramVal = superParamValues.get(snRhs.getIdentifier());
} else if (rhsResolver != null) {
paramVal = rhsResolver.resolve(rhs, superParamValues, superTd);
}
if (paramVal != null) {
fieldValues.put(fieldName, paramVal);
fieldValues.put("this." + fieldName, paramVal);
}
}
return super.visit(node);
}
});
// Recursively follow further super() calls, passing down the current superclass paramValues
superCtor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.SuperConstructorInvocation sci) {
String superFqn = context.getSuperclassFqn(superTd);
if (superFqn != null) {
TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
if (nextSuperTd != null) {
processSuperConstructorArgs(nextSuperTd, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
}
}
return super.visit(sci);
}
});
break; // first matching super constructor is enough
}
}
/**
* Evaluates a method call expression found in a constructor body (e.g., {@code this.field = computeValue(param)}).
* Handles both explicit {@code this.} and implicit {@code this} (null receiver) calls.
*
* @param rhs the expression to evaluate (expected to be a MethodInvocation)
* @param paramValues map of constructor parameter names to their resolved values
* @param td the type declaration containing the method
* @return the resolved value, or {@code null} if evaluation is not possible
*/
protected String evaluateMethodCallInConstructor(
org.eclipse.jdt.core.dom.Expression rhs,
java.util.Map<String, String> paramValues,
org.eclipse.jdt.core.dom.TypeDeclaration td) {
if (!(rhs instanceof org.eclipse.jdt.core.dom.MethodInvocation mi)) {
return null;
}
String methodName = mi.getName().getIdentifier();
org.eclipse.jdt.core.dom.Expression receiver = mi.getExpression();
// Handle both explicit 'this.' and implicit 'this' (null receiver)
if (receiver != null && !(receiver instanceof org.eclipse.jdt.core.dom.ThisExpression)) {
return null;
}
org.eclipse.jdt.core.dom.MethodDeclaration method = context.findMethodDeclaration(td, methodName, true);
if (method == null || method.getBody() == null) {
return null;
}
java.util.Map<String, String> localVars = new java.util.HashMap<>(paramValues);
return constantResolver.evaluateMethodBodyWithLocals(method, localVars, context, new java.util.HashSet<>());
} }
/** /**
@@ -1193,7 +1469,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return -1; return -1;
} }
private void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List<String> constants) { protected void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List<String> constants) {
if (argObj instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation innerCic) { if (argObj instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation innerCic) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType()); String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
if ("String".equals(typeName)) { if ("String".equals(typeName)) {
@@ -1323,6 +1599,62 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return null; return null;
} }
protected String traceLocalVariable(String methodFqn, String varName, java.util.Map<String, String> parameterValues) {
String result = traceLocalVariable(methodFqn, varName);
if (result != null && parameterValues != null && !parameterValues.isEmpty()) {
TypeDeclaration td = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
if (td != null) {
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
final org.eclipse.jdt.core.dom.ASTNode[] switchNode = new org.eclipse.jdt.core.dom.ASTNode[1];
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
Expression init = node.getInitializer();
if (init.getNodeType() == ASTNode.SWITCH_EXPRESSION) {
switchNode[0] = init;
return false;
}
if (init.getNodeType() == ASTNode.SWITCH_STATEMENT) {
switchNode[0] = init;
return false;
}
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
Expression rhs = node.getRightHandSide();
if (rhs.getNodeType() == ASTNode.SWITCH_EXPRESSION) {
switchNode[0] = rhs;
return false;
}
if (rhs.getNodeType() == ASTNode.SWITCH_STATEMENT) {
switchNode[0] = rhs;
return false;
}
}
return super.visit(node);
}
});
if (switchNode[0] != null) {
if (switchNode[0] instanceof SwitchExpression se) {
String evaluated = constantResolver.evaluateSwitchWithParams(se, parameterValues, context);
if (evaluated != null) return evaluated;
} else if (switchNode[0] instanceof SwitchStatement ss) {
String evaluated = constantResolver.evaluateSwitchWithParams(ss, parameterValues, context);
if (evaluated != null) return evaluated;
}
}
}
}
}
return result;
}
protected org.eclipse.jdt.core.dom.Expression traceLocalSetter(String methodFqn, String varName, String getterName) { protected org.eclipse.jdt.core.dom.Expression traceLocalSetter(String methodFqn, String varName, String getterName) {
if (methodFqn == null || !methodFqn.contains(".")) return null; if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
@@ -1464,6 +1796,81 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal; return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
} }
/**
* Resolves a single argument expression to a string value.
* Handles lambdas, method references, and constructor argument unwrapping.
* Subclasses can override {@link #postProcessResolvedArgument(Expression, String)} for custom behavior.
*/
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
// Extract from lambda
if (expr instanceof org.eclipse.jdt.core.dom.LambdaExpression le) {
org.eclipse.jdt.core.dom.ASTNode body = le.getBody();
if (body instanceof org.eclipse.jdt.core.dom.Expression bodyExpr) {
expr = bodyExpr;
} else if (body instanceof org.eclipse.jdt.core.dom.Block block) {
for (Object stmtObj : block.statements()) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
// Extract from method reference
if (expr instanceof org.eclipse.jdt.core.dom.ExpressionMethodReference emr) {
org.eclipse.jdt.core.dom.TypeDeclaration td = findEnclosingType(emr);
if (td != null) {
org.eclipse.jdt.core.dom.MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
if (md != null && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
}
// Unwrap constructor first argument if it's a constant (e.g., new CustomMessage(OrderEvent.PROCESS))
if (expr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
org.eclipse.jdt.core.dom.Expression firstArg = (org.eclipse.jdt.core.dom.Expression) cic.arguments().get(0);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg;
}
}
String val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
}
// Allow subclasses to post-process (e.g., preserve getters for later substitution)
return postProcessResolvedArgument(expr, val);
}
/**
* Hook for subclasses to customize argument resolution.
* Default implementation returns the value as-is.
*/
protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
return resolvedValue;
}
/**
* Resolves a list of argument expressions.
* Default implementation calls {@link #resolveArgument(Expression)} for each.
*/
protected List<String> resolveArguments(List<?> astArguments) {
List<String> args = new ArrayList<>();
for (Object argObj : astArguments) {
args.add(resolveArgument((org.eclipse.jdt.core.dom.Expression) argObj));
}
return args;
}
protected abstract Map<String, List<CallEdge>> buildCallGraph(); protected abstract Map<String, List<CallEdge>> buildCallGraph();
protected abstract String resolveCalledMethod(MethodInvocation node); protected abstract String resolveCalledMethod(MethodInvocation node);
protected List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) { protected List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {

View File

@@ -35,7 +35,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
List<String> calledMethods = resolveCalledMethodsPolymorphic(node); List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
List<String> args = resolveArguments(node.arguments()); List<String> args = resolveArguments(node.arguments());
for (String calledMethod : calledMethods) { for (String calledMethod : calledMethods) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
} }
for (Object argObj : node.arguments()) { for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) { if (argObj instanceof ExpressionMethodReference emr) {
@@ -51,7 +52,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} else { } else {
implicitArgs.addAll(args); implicitArgs.addAll(args);
} }
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs)); String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs, receiver));
} }
} }
} else { } else {
@@ -70,11 +72,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} else { } else {
implicitArgs.addAll(args); implicitArgs.addAll(args);
} }
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs)); String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs, receiver));
List<String> impls = context.getImplementations(fallbackTypeFqn); List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) { for (String impl : impls) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args)); graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver));
} }
} }
} }
@@ -106,7 +109,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
calledMethod = superFqn + "." + methodName; calledMethod = superFqn + "." + methodName;
} }
List<String> args = resolveArguments(node.arguments()); List<String> args = resolveArguments(node.arguments());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); String receiver = "super";
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
} }
} }
} }
@@ -118,70 +122,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
return graph; return graph;
} }
protected List<String> resolveArguments(List<?> astArguments) { @Override
List<String> args = new ArrayList<>(); protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
for (Object argObj : astArguments) { // Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
Expression expr = (Expression) argObj; // where we can substitute the actual receiver from the call edge
boolean isThisOrSuperGetter = originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi
&& (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression || mi.getExpression() instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation)
&& mi.arguments().isEmpty();
// Extract from lambda if (isThisOrSuperGetter) {
if (expr instanceof LambdaExpression le) { return originalExpr.toString(); // Preserve "this.getTransitionType()" for later substitution
ASTNode body = le.getBody();
if (body instanceof Expression bodyExpr) {
expr = bodyExpr;
} else if (body instanceof Block block) {
for (Object stmtObj : block.statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
} }
}
}
}
if (expr instanceof ExpressionMethodReference emr) {
TypeDeclaration td = findEnclosingType(expr);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
if (md != null && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
}
// Removed eager tracing here to allow dynamic tracing in resolveTriggerPointParameters
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
Expression firstArg = (Expression) cic.arguments().get(0);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg; // Only unwrap if it's actually a constant
}
}
String val = null;
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized // Narrow resolution for "localVar.getX()" patterns where localVar is initialized
// from a ClassInstanceCreation (directly or via a factory method). This avoids // from a ClassInstanceCreation (directly or via a factory method). This avoids
// the broad ENUM_SET fallback when the getter transforms one enum to another. // the broad ENUM_SET fallback when the getter transforms one enum to another.
if (expr instanceof MethodInvocation getterMi if (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation getterMi
&& getterMi.getExpression() instanceof SimpleName instanceSn && getterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName instanceSn
&& getterMi.arguments().isEmpty()) { && getterMi.arguments().isEmpty()) {
val = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn); String narrowed = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
if (narrowed != null) {
return narrowed;
}
} }
if (val == null) val = constantResolver.resolve(expr, context); return resolvedValue;
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
}
args.add(val);
}
System.out.println("DEBUG resolveArguments: " + args);
return args;
} }
protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) { protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
@@ -195,6 +160,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
String definingClass = findDefiningClass(className, methodName);
if (definingClass != null && !definingClass.equals(className)) {
allResolved.set(0, definingClass + "." + methodName);
className = definingClass;
}
List<String> impls = context.getImplementations(className); List<String> impls = context.getImplementations(className);
for (String impl : impls) { for (String impl : impls) {
allResolved.add(impl + "." + methodName); allResolved.add(impl + "." + methodName);
@@ -204,6 +175,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
return allResolved; return allResolved;
} }
private String findDefiningClass(String className, String methodName) {
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) return null;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
if (md != null) {
return className;
}
String superFqn = context.getSuperclassFqn(td);
while (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
MethodDeclaration superMd = context.findMethodDeclaration(superTd, methodName, false);
if (superMd != null) {
return superFqn;
}
superFqn = context.getSuperclassFqn(superTd);
} else {
break;
}
}
return null;
}
protected String resolveCalledMethod(MethodInvocation node) { protected String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression(); Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier(); String methodName = node.getName().getIdentifier();
@@ -389,7 +385,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
if (varTypeTd == null) return null; if (varTypeTd == null) return null;
// Evaluate the getter body with field values substituted from the CIC's constructor args // Evaluate the getter body with field values substituted from the CIC's constructor args
java.util.Map<String, String> fieldValues = buildFieldValuesFromCIC(varTypeTd, cic, context); java.util.Map<String, String> fieldValues = buildFieldValuesFromCIC(varTypeTd, cic, context, this::evaluateMethodCallInConstructor);
if (fieldValues.isEmpty()) return null; if (fieldValues.isEmpty()) return null;
// Track setter calls on the variable between its initialization and the getter call // Track setter calls on the variable between its initialization and the getter call
@@ -487,4 +483,446 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} }
} }
} }
/**
* Extracts a string representation of the receiver expression from a MethodInvocation.
* Handles ThisExpression, SimpleName, and other expression types.
*/
protected String getReceiverString(Expression receiver) {
if (receiver == null) {
return null;
}
if (receiver instanceof ThisExpression) {
return "this";
}
if (receiver instanceof SuperMethodInvocation) {
return "super";
}
if (receiver instanceof SimpleName sn) {
return sn.getIdentifier();
}
return receiver.toString();
}
/**
* Finds the receiver expression used in the caller method when calling the target method.
* Walks the caller's AST to find a MethodInvocation with the target method name
* and returns the receiver string.
*/
protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
int lastDot = callerFqn.lastIndexOf('.');
if (lastDot < 0) return null;
String callerClass = callerFqn.substring(0, lastDot);
String callerMethod = callerFqn.substring(lastDot + 1);
int targetLastDot = targetMethodFqn.lastIndexOf('.');
if (targetLastDot < 0) return null;
String targetMethodName = targetMethodFqn.substring(targetLastDot + 1);
TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
if (callerTd == null) return null;
MethodDeclaration callerMd = context.findMethodDeclaration(callerTd, callerMethod, false);
if (callerMd == null || callerMd.getBody() == null) return null;
final String[] foundReceiver = {null};
callerMd.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if (foundReceiver[0] != null) return false;
String called = resolveCalledMethod(node);
if (called != null && (called.equals(targetMethodFqn) || called.endsWith("." + targetMethodName))) {
foundReceiver[0] = getReceiverString(node.getExpression());
return false;
}
return super.visit(node);
}
});
return foundReceiver[0];
}
@Override
protected 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 = "";
boolean didThisSuperSubstitution = false;
String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix);
currentParamName = extractedEntry[0];
methodSuffix = extractedEntry[1];
for (int i = path.size() - 1; i > 0; i--) {
String target = path.get(i);
String caller = path.get(i - 1);
int paramIndex = getParameterIndex(target, currentParamName);
if (paramIndex < 0) {
java.util.Map<String, String> parameterValues = buildParameterValuesMap(caller, target, callGraph, path, i);
String tracedVar = traceLocalVariable(target, currentParamName, parameterValues);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix);
tracedVar = extractedTraced[0];
methodSuffix = extractedTraced[1];
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
paramIndex = getParameterIndex(target, currentParamName);
}
}
if (paramIndex < 0) {
break;
}
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) {
System.out.println("DEBUG TRACE: caller=" + caller + " target=" + target + " paramIndex=" + paramIndex + " arg=" + arg + " receiver=" + edge.getReceiver());
String receiver = edge.getReceiver();
if (receiver != null && !receiver.isEmpty() && ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super."))) {
String actualReceiver = receiver;
boolean needPrevReceiver = "this".equals(arg) || arg.startsWith("this.");
if (needPrevReceiver && !"this".equals(receiver) && !"super".equals(receiver)) {
if (i - 1 > 0) {
String prevCaller = path.get(i - 2);
List<CallEdge> prevEdges = callGraph.get(prevCaller);
if (prevEdges != null) {
for (CallEdge prevEdge : prevEdges) {
if (prevEdge.getTargetMethod().equals(caller) || isHeuristicMatch(prevEdge.getTargetMethod(), caller)) {
actualReceiver = prevEdge.getReceiver();
System.out.println("DEBUG TRACE: found prev edge, actualReceiver=" + actualReceiver);
break;
}
}
}
}
} else if ("this".equals(receiver) || "super".equals(receiver)) {
if (i - 1 > 0) {
String prevCaller = path.get(i - 2);
List<CallEdge> prevEdges = callGraph.get(prevCaller);
if (prevEdges != null) {
for (CallEdge prevEdge : prevEdges) {
if (prevEdge.getTargetMethod().equals(caller) || isHeuristicMatch(prevEdge.getTargetMethod(), caller)) {
actualReceiver = prevEdge.getReceiver();
System.out.println("DEBUG TRACE: found prev edge, actualReceiver=" + actualReceiver);
break;
}
}
}
}
}
if (actualReceiver != null && !actualReceiver.isEmpty()) {
if ("this".equals(arg) || arg.startsWith("this.")) {
arg = arg.replaceFirst("^this", actualReceiver);
didThisSuperSubstitution = true;
} else if ("super".equals(arg) || arg.startsWith("super.")) {
arg = arg.replaceFirst("^super", actualReceiver);
didThisSuperSubstitution = true;
}
System.out.println("DEBUG TRACE: after substitution arg=" + arg);
}
}
String[] extractedArg = extractMethodSuffix(arg, methodSuffix);
arg = extractedArg[0];
methodSuffix = extractedArg[1];
currentParamName = arg;
resolvedValue = arg + methodSuffix;
found = true;
break;
}
}
}
}
}
if (!found) break;
}
String entryMethod = path.get(0);
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
if (resolvedValue != null && resolvedValue.contains(".") && !resolvedValue.contains("(") && !resolvedValue.contains(" ")) {
TriggerPoint modifiedTp = TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.build();
return super.resolveTriggerPointParameters(modifiedTp, path, callGraph);
}
if (entryParamIndex < 0) {
if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) {
String localMethodName = methodSuffix.substring(1).replace("()", "");
org.eclipse.jdt.core.dom.Expression localSetterExpr = traceLocalSetter(entryMethod, currentParamName, localMethodName);
if (localSetterExpr != null) {
List<String> setterEvents = new ArrayList<>();
List<org.eclipse.jdt.core.dom.Expression> tracedSetters = traceVariableAll(localSetterExpr);
for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) {
extractConstantsFromExpression(tracedSetter, setterEvents);
}
if (!setterEvents.isEmpty()) {
return TriggerPoint.builder()
.event(tp.getEvent())
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(setterEvents)
.build();
}
}
}
boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null
&& !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new ");
if (!hasGetterOnReceiver) {
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
tracedVar = extractedFinalTraced[0];
methodSuffix = extractedFinalTraced[1];
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
}
}
}
String finalResolvedValue = resolvedValue;
boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null
&& !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new ");
if (hasGetterOnReceiver && didThisSuperSubstitution) {
List<String> getterEvents = evaluateGetterOnReceiver(finalResolvedValue, methodSuffix, entryMethod, path, callGraph);
if (getterEvents != null && !getterEvents.isEmpty()) {
return TriggerPoint.builder()
.event(tp.getEvent())
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(getterEvents)
.build();
}
// If we couldn't evaluate the getter, fall back to super with modified trigger point
TriggerPoint modifiedTp = TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.build();
return super.resolveTriggerPointParameters(modifiedTp, path, callGraph);
}
return super.resolveTriggerPointParameters(tp, path, callGraph);
}
private List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
System.out.println("DEBUG evaluateGetterOnReceiver: resolvedValue=" + resolvedValue + " methodSuffix=" + methodSuffix + " entryMethod=" + entryMethod);
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.setSource(resolvedValue.toCharArray());
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null);
if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
String getterName = mi.getName().getIdentifier();
org.eclipse.jdt.core.dom.Expression receiver = mi.getExpression();
String receiverName = null;
String receiverType = null;
if (receiver instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
receiverName = sn.getIdentifier();
receiverType = getVariableDeclaredType(entryMethod, receiverName);
System.out.println("DEBUG evaluateGetterOnReceiver: receiverName=" + receiverName + " receiverType=" + receiverType);
}
if (receiverType == null && path.size() > 1) {
String callerMethod = path.get(1);
receiverType = getVariableDeclaredType(callerMethod, receiverName);
System.out.println("DEBUG evaluateGetterOnReceiver: fallback receiverType=" + receiverType);
}
if (receiverType != null) {
String simpleReceiverType = receiverType;
if (receiverType.contains("<")) {
simpleReceiverType = receiverType.substring(0, receiverType.indexOf('<'));
}
System.out.println("DEBUG evaluateGetterOnReceiver: simpleReceiverType=" + simpleReceiverType);
TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType);
if (td != null) {
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
if (getter != null && getter.getBody() != null) {
Map<String, String> fieldValues = new HashMap<>();
String varName = receiverName;
org.eclipse.jdt.core.dom.Expression initExpr = findVariableInitializer(entryMethod, varName);
System.out.println("DEBUG evaluateGetterOnReceiver: initExpr=" + initExpr + " class=" + (initExpr != null ? initExpr.getClass().getName() : "null"));
if (initExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor));
System.out.println("DEBUG evaluateGetterOnReceiver: fieldValues=" + fieldValues);
int lastDot = entryMethod.lastIndexOf('.');
if (lastDot > 0) {
String className = entryMethod.substring(0, lastDot);
String methodName = entryMethod.substring(lastDot + 1);
TypeDeclaration entryTd = context.getTypeDeclaration(className);
if (entryTd != null) {
MethodDeclaration entryMd = context.findMethodDeclaration(entryTd, methodName, false);
if (entryMd != null && entryMd.getBody() != null) {
// Skip trackSetterCallsBetween since getter call is not in entry method
}
}
}
}
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
System.out.println("DEBUG evaluateGetterOnReceiver: result=" + result);
if (result != null) {
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
if (returnType != null && !result.contains(".")) {
int lastDot = returnType.lastIndexOf('.');
String simpleReturnType = lastDot > 0 ? returnType.substring(lastDot + 1) : returnType;
result = simpleReturnType + "." + result;
}
return Collections.singletonList(result);
}
}
}
}
}
return null;
}
private org.eclipse.jdt.core.dom.Expression findVariableInitializer(String methodFqn, String varName) {
int lastDot = methodFqn.lastIndexOf('.');
if (lastDot < 0) return null;
String className = methodFqn.substring(0, lastDot);
String methodName = methodFqn.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) return null;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
if (md == null || md.getBody() == null) return null;
final org.eclipse.jdt.core.dom.Expression[] initExpr = {null};
md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName) && frag.getInitializer() != null) {
initExpr[0] = frag.getInitializer();
return false;
}
}
return super.visit(node);
}
});
return initExpr[0];
}
private java.util.Map<String, String> buildParameterValuesMap(String caller, String target, Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
java.util.Map<String, String> paramValues = new java.util.HashMap<>();
List<CallEdge> edges = callGraph.get(caller);
if (edges == null) {
return paramValues;
}
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
List<String> args = edge.getArguments();
if (args == null || args.isEmpty()) break;
int lastDot = target.lastIndexOf('.');
if (lastDot < 0) break;
String className = target.substring(0, lastDot);
String methodName = target.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) break;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null) break;
List<String> paramNames = new java.util.ArrayList<>();
for (Object paramObj : md.parameters()) {
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) paramObj;
paramNames.add(param.getName().getIdentifier());
}
int minSize = java.lang.Math.min(paramNames.size(), args.size());
for (int j = 0; j < minSize; j++) {
String argValue = args.get(j);
String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph);
paramValues.put(paramNames.get(j), resolvedArgValue);
}
break;
}
}
return paramValues;
}
private String resolveArgumentValue(String argValue, String callerMethod, List<String> path, int pathIndex, Map<String, List<CallEdge>> callGraph) {
if (argValue == null) return null;
if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*")) {
return argValue;
}
int callerIndex = path.indexOf(callerMethod);
if (callerIndex <= 0) return argValue;
String prevCaller = path.get(callerIndex - 1);
List<CallEdge> prevEdges = callGraph.get(prevCaller);
if (prevEdges == null) return argValue;
for (CallEdge edge : prevEdges) {
if (edge.getTargetMethod().equals(callerMethod) || isHeuristicMatch(edge.getTargetMethod(), callerMethod)) {
List<String> prevArgs = edge.getArguments();
if (prevArgs == null || prevArgs.isEmpty()) break;
int lastDot = callerMethod.lastIndexOf('.');
if (lastDot < 0) break;
String className = callerMethod.substring(0, lastDot);
String methodName = callerMethod.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) break;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null) break;
List<String> paramNames = new java.util.ArrayList<>();
for (Object paramObj : md.parameters()) {
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) paramObj;
paramNames.add(param.getName().getIdentifier());
}
int paramIdx = paramNames.indexOf(argValue);
if (paramIdx >= 0 && paramIdx < prevArgs.size()) {
String prevArg = prevArgs.get(paramIdx);
return resolveArgumentValue(prevArg, prevCaller, path, pathIndex, callGraph);
}
break;
}
}
return argValue;
}
} }

View File

@@ -122,67 +122,25 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
return graph; return graph;
} }
private List<String> resolveArguments(List<?> astArguments) { @Override
List<String> args = new ArrayList<>(); protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
for (Object argObj : astArguments) {
Expression expr = (Expression) argObj;
// Extract from lambda
if (expr instanceof LambdaExpression le) {
ASTNode body = le.getBody();
if (body instanceof Expression bodyExpr) {
expr = bodyExpr;
} else if (body instanceof Block block) {
for (Object stmtObj : block.statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
if (expr instanceof ExpressionMethodReference emr) {
TypeDeclaration td = findEnclosingType(expr);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
if (md != null && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
}
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...)) // Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
Expression unwrappedBuilder = unwrapMessageBuilder(expr); org.eclipse.jdt.core.dom.Expression unwrappedBuilder = unwrapMessageBuilder(expr);
if (unwrappedBuilder != expr) { if (unwrappedBuilder != expr) {
expr = unwrappedBuilder; expr = unwrappedBuilder;
} }
Expression tracedExpr = traceVariable(expr); // Trace variable to resolve local variable references
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) { org.eclipse.jdt.core.dom.Expression tracedExpr = traceVariable(expr);
if (tracedExpr instanceof org.eclipse.jdt.core.dom.QualifiedName
|| tracedExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation
|| tracedExpr instanceof org.eclipse.jdt.core.dom.StringLiteral
|| tracedExpr instanceof org.eclipse.jdt.core.dom.NumberLiteral) {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
} }
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) { // Delegate to base class for lambda, method reference, and constructor unwrapping
Expression firstArg = (Expression) cic.arguments().get(0); return super.resolveArgument(expr);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg; // Only unwrap if it's actually a constant
}
}
String val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
}
args.add(val);
}
return args;
} }
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) { private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {

View File

@@ -0,0 +1,230 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineExtendedTest {
/**
* Diagnostic: Generics, inherited processing, multi-enum constructor args,
* and internal constructor mapping via a switch method.
* <p>
* Pattern:
* 1. Abstract base class AbstractEventClass<T> holds the payload and transitionType.
* 2. Abstract base class also contains the method that actually fires the state machine.
* 3. MyEventClass<T> constructor takes multiple enums (InputEnum1, InputEnum2).
* 4. MyEventClass assigns: this.transitionType = computeTransitionType(InputEnum2).
* 5. computeTransitionType uses a switch to map InputEnum2 to TransitionEnum.
* <p>
* Root cause targeted: The engine must not eagerly grab InputEnum1.IGNORE or
* InputEnum2.SOURCE_B. It must evaluate the internal computeTransitionType()
* method during instantiation to resolve the final TransitionEnum.STATE_Y.
*/
@Test
void shouldResolveTransitionEnumThroughGenericsAndInternalConstructorSwitchComputation() throws IOException {
String source = """
package com.example;
public class SystemController {
private StateMachine machine;
public void processIncomingPayload(String rawData) {
// Creates concrete class with multiple enums and a generic payload.
// Engine MUST NOT stop at IGNORE or SOURCE_B.
MyEventClass<String> event = new MyEventClass<>(
rawData,
InputEnum1.IGNORE,
InputEnum2.SOURCE_B
);
// Calls the inherited method that actually processes the event
event.fireEvent(machine);
}
}
abstract class AbstractEventClass<T> {
protected T payload;
protected TransitionEnum transitionType;
public AbstractEventClass(T payload) {
this.payload = payload;
}
// The method to actually process events
public void fireEvent(StateMachine machine) {
machine.fire(this.getTransitionType());
}
public TransitionEnum getTransitionType() {
return this.transitionType;
}
}
class MyEventClass<T> extends AbstractEventClass<T> {
private InputEnum1 routingFlag;
public MyEventClass(T payload, InputEnum1 flag, InputEnum2 source) {
super(payload);
this.routingFlag = flag;
// The trap: Assignment via an internal method evaluation
this.transitionType = computeTransitionType(source);
}
private TransitionEnum computeTransitionType(InputEnum2 source) {
return switch (source) {
case SOURCE_A -> TransitionEnum.STATE_X;
case SOURCE_B -> TransitionEnum.STATE_Y; // Expected resolution
case SOURCE_C -> TransitionEnum.STATE_Z;
};
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum InputEnum1 { IGNORE, PROCESS_ASYNC }
enum InputEnum2 { SOURCE_A, SOURCE_B, SOURCE_C }
enum TransitionEnum { STATE_X, STATE_Y, STATE_Z }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_generics_computation");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.SystemController")
.methodName("processIncomingPayload")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("GENERICS & COMPUTATION polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// The assertion to prove the bug is fixed.
// If the engine returns InputEnum2.SOURCE_B or InputEnum1.IGNORE, it fails.
// It MUST evaluate computeTransitionType() and return TransitionEnum.STATE_Y.
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("TransitionEnum.STATE_Y");
}
/**
* Diagnostic: Enum tracked through an overridden method and a super() call.
*
* Pattern:
* 1. EntryPoint calls an overridden method on a subclass (CustomHandler.handleEvent)
* with an initial enum (ActionType.BEGIN_CHECKOUT).
* 2. The subclass intercepts the call, does some localized logic, and delegates
* back to the parent using super.handleEvent(ActionType).
* 3. The parent class (BaseHandler) takes that enum, transforms it via a switch
* statement to a TransitionEnum, and fires the state machine.
*
* Root cause targeted: The engine must successfully resolve the `super` keyword
* to the parent class's AST node, maintain the argument mapping across the inheritance
* boundary, and evaluate the switch to get the final TransitionEnum. It must not
* get stuck in a recursive loop or eagerly return ActionType.BEGIN_CHECKOUT.
*/
@Test
void shouldResolveMappedEnumAcrossOverriddenMethodAndSuperDelegation() throws IOException {
String source = """
package com.example;
public class ApiEndpoint {
private CustomHandler handler;
public void triggerCheckout() {
// Entry point passes the initial enum
handler.handleEvent(ActionType.BEGIN_CHECKOUT);
}
}
abstract class BaseHandler {
protected StateMachine machine;
// The method that gets overridden, but ultimately does the work
public void handleEvent(ActionType action) {
TransitionEnum transition = switch (action) {
case BEGIN_CHECKOUT -> TransitionEnum.CHECKOUT_STARTED; // Expected
case ABORT_CHECKOUT -> TransitionEnum.CHECKOUT_CANCELLED;
};
machine.fire(transition);
}
}
class CustomHandler extends BaseHandler {
@Override
public void handleEvent(ActionType action) {
// Custom logic before delegation (simulated)
if (action == null) return;
// The trap: The engine must follow 'super' to BaseHandler
// and carry the 'action' argument with it.
super.handleEvent(action);
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum ActionType { BEGIN_CHECKOUT, ABORT_CHECKOUT }
enum TransitionEnum { CHECKOUT_STARTED, CHECKOUT_CANCELLED }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_super_override");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.ApiEndpoint")
.methodName("triggerCheckout")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("SUPER DELEGATION polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// The assertion to prove the bug is fixed.
// It MUST NOT contain "ActionType.BEGIN_CHECKOUT".
// It MUST successfully trace through super.handleEvent() and resolve the switch.
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("TransitionEnum.CHECKOUT_STARTED");
}
}

View File

@@ -2991,4 +2991,112 @@ class HeuristicCallGraphEngineTest {
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED"); .containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED");
} }
/**
* Diagnostic: Complex multi-argument initialization with abstract base class mapping.
*
* Pattern:
* 1. A Factory creates a specific subclass (WebCommand), passing multiple arguments
* including an intermediate enum (SourceType.WEB_API) alongside strings and ints.
* 2. The subclass delegates to an abstract BaseCommand via super(...).
* 3. The base class stores the intermediate enum in a protected field.
* 4. The controller calls an inherited method: cmd.getTransitionEvent()
* 5. The inherited method uses a switch on the protected field to return a TransitionEnum.
*
* Root cause targeted: The engine traces the field back to the constructor and finds
* SourceType.WEB_API. If the engine is buggy, it will eagerly return "SourceType.WEB_API".
* If correct, it will evaluate the switch mapping and return "TransitionEnum.ON_WEB_START".
*/
@Test
void shouldResolveMappedTransitionEnumThroughAbstractBaseClassAndMultipleArgs() throws IOException {
String source = """
package com.example;
public class FlowController {
private CommandFactory factory;
private StateMachine machine;
public void handleRequest(String payload) {
// Creates subclass with multiple args, including intermediate enum
BaseCommand cmd = factory.buildCommand(payload);
// Fires using the mapped enum from the inherited base method
machine.fire(cmd.getTransitionEvent());
}
}
class CommandFactory {
public BaseCommand buildCommand(String payload) {
// Passing multiple args: String, Enum1, String, int
return new WebCommand(payload, SourceType.WEB_API, "meta_v1", 42);
}
}
abstract class BaseCommand {
protected String id;
protected SourceType sourceType; // The intermediate enum
protected String metadata;
public BaseCommand(String id, SourceType sourceType, String metadata) {
this.id = id;
this.sourceType = sourceType;
this.metadata = metadata;
}
// The tricky part: Inherited method doing the mapping
public TransitionEnum getTransitionEvent() {
return switch (this.sourceType) {
case WEB_API -> TransitionEnum.ON_WEB_START;
case MOBILE_APP -> TransitionEnum.ON_MOBILE_START;
case CRON_JOB -> TransitionEnum.ON_SYSTEM_START;
};
}
}
class WebCommand extends BaseCommand {
private int priorityLevel;
public WebCommand(String id, SourceType sourceType, String metadata, int priorityLevel) {
super(id, sourceType, metadata); // Engine must track through super()
this.priorityLevel = priorityLevel;
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum SourceType { WEB_API, MOBILE_APP, CRON_JOB }
enum TransitionEnum { ON_WEB_START, ON_MOBILE_START, ON_SYSTEM_START }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_complex_inheritance");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.FlowController")
.methodName("handleRequest")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("COMPLEX INHERITANCE polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// The assertion to prove the bug is fixed.
// It MUST NOT contain "SourceType.WEB_API"
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("TransitionEnum.ON_WEB_START");
}
} }