heuristics update
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CallEdge {
|
||||
private final String targetMethod;
|
||||
private final List<String> arguments;
|
||||
private String targetMethod;
|
||||
private List<String> arguments;
|
||||
private String receiver;
|
||||
|
||||
public CallEdge(String targetMethod, List<String> arguments) {
|
||||
this(targetMethod, arguments, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
this.constantResolver = new ConstantResolver();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
protected interface RhsResolver {
|
||||
String resolve(Expression rhs, Map<String, String> paramValues, TypeDeclaration td);
|
||||
}
|
||||
|
||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
||||
List<CallChain> chains = new ArrayList<>();
|
||||
@@ -986,13 +991,48 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
* field assignments found in the matching constructor body.
|
||||
*
|
||||
* <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(
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration td,
|
||||
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
|
||||
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()) {
|
||||
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
|
||||
@@ -1029,12 +1069,17 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
fieldName = fa.getName().getIdentifier();
|
||||
} else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn
|
||||
&& !paramValues.containsKey(sn.getIdentifier())) {
|
||||
// Bare field assignment (not a parameter-to-itself assignment)
|
||||
fieldName = sn.getIdentifier();
|
||||
}
|
||||
|
||||
if (fieldName != null && rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
|
||||
String paramVal = paramValues.get(snRhs.getIdentifier());
|
||||
if (fieldName != null) {
|
||||
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) {
|
||||
fieldValues.put(fieldName, paramVal);
|
||||
fieldValues.put("this." + fieldName, paramVal);
|
||||
@@ -1043,9 +1088,152 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
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
|
||||
}
|
||||
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 +1381,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
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) {
|
||||
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
|
||||
if ("String".equals(typeName)) {
|
||||
@@ -1464,6 +1652,81 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
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 String resolveCalledMethod(MethodInvocation node);
|
||||
protected List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||
|
||||
@@ -35,7 +35,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
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()) {
|
||||
if (argObj instanceof ExpressionMethodReference emr) {
|
||||
@@ -51,7 +52,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
} else {
|
||||
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 {
|
||||
@@ -70,11 +72,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
} else {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
protected List<String> resolveArguments(List<?> astArguments) {
|
||||
List<String> args = new ArrayList<>();
|
||||
for (Object argObj : astArguments) {
|
||||
Expression expr = (Expression) argObj;
|
||||
@Override
|
||||
protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
|
||||
// Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
|
||||
// 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 (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 (isThisOrSuperGetter) {
|
||||
return originalExpr.toString(); // Preserve "this.getTransitionType()" for later substitution
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// from a ClassInstanceCreation (directly or via a factory method). This avoids
|
||||
// the broad ENUM_SET fallback when the getter transforms one enum to another.
|
||||
if (expr instanceof MethodInvocation getterMi
|
||||
&& getterMi.getExpression() instanceof SimpleName instanceSn
|
||||
if (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation getterMi
|
||||
&& getterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName instanceSn
|
||||
&& 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);
|
||||
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;
|
||||
return resolvedValue;
|
||||
}
|
||||
|
||||
protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||
@@ -195,6 +160,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||
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);
|
||||
for (String impl : impls) {
|
||||
allResolved.add(impl + "." + methodName);
|
||||
@@ -204,6 +175,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
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) {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
@@ -389,7 +385,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (varTypeTd == null) return null;
|
||||
|
||||
// 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;
|
||||
|
||||
// Track setter calls on the variable between its initialization and the getter call
|
||||
@@ -487,4 +483,343 @@ 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) {
|
||||
String tracedVar = traceLocalVariable(target, currentParamName);
|
||||
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 (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];
|
||||
}
|
||||
}
|
||||
@@ -122,67 +122,25 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return graph;
|
||||
}
|
||||
|
||||
private List<String> resolveArguments(List<?> astArguments) {
|
||||
List<String> args = new ArrayList<>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
|
||||
// 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) {
|
||||
expr = unwrappedBuilder;
|
||||
}
|
||||
|
||||
Expression tracedExpr = traceVariable(expr);
|
||||
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
||||
// Trace variable to resolve local variable references
|
||||
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
|
||||
}
|
||||
|
||||
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 = constantResolver.resolve(expr, context);
|
||||
if (val == null) {
|
||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
||||
}
|
||||
args.add(val);
|
||||
}
|
||||
return args;
|
||||
// Delegate to base class for lambda, method reference, and constructor unwrapping
|
||||
return super.resolveArgument(expr);
|
||||
}
|
||||
|
||||
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -2991,4 +2991,112 @@ class HeuristicCallGraphEngineTest {
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user