heuristics update
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
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 +991,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 +1069,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 +1088,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 +1381,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)) {
|
||||||
@@ -1464,6 +1652,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) {
|
||||||
|
|||||||
@@ -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,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;
|
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) {
|
||||||
|
|||||||
@@ -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())
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user