Compare commits
2 Commits
ai-branch-
...
fcda626629
| Author | SHA1 | Date | |
|---|---|---|---|
| fcda626629 | |||
| e31056a44e |
@@ -27,7 +27,7 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
|||||||
hasPolyMatch = true;
|
hasPolyMatch = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continue; // Stricter matching: do not fallback if both have qualifiers but don't match
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String simplePe = pe;
|
String simplePe = pe;
|
||||||
|
|||||||
@@ -56,6 +56,22 @@ public class SymbolicPathValueEstimator implements PathValueEstimator {
|
|||||||
return super.visit(node);
|
return super.visit(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(InfixExpression node) {
|
||||||
|
if (node.getOperator() == InfixExpression.Operator.EQUALS) {
|
||||||
|
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||||
|
String left = resolver.resolve(node.getLeftOperand(), context);
|
||||||
|
if (left != null && !left.isEmpty()) {
|
||||||
|
addValue(collectedConstants, left);
|
||||||
|
}
|
||||||
|
String right = resolver.resolve(node.getRightOperand(), context);
|
||||||
|
if (right != null && !right.isEmpty()) {
|
||||||
|
addValue(collectedConstants, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(SwitchStatement node) {
|
public boolean visit(SwitchStatement node) {
|
||||||
for (Object stmt : node.statements()) {
|
for (Object stmt : node.statements()) {
|
||||||
|
|||||||
@@ -108,7 +108,32 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
TypeDeclaration td = findEnclosingType(mi);
|
TypeDeclaration td = null;
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
ITypeBinding typeBinding = mi.getExpression().resolveTypeBinding();
|
||||||
|
if (typeBinding != null) {
|
||||||
|
String typeName = typeBinding.getQualifiedName();
|
||||||
|
if (typeName != null && !typeName.isEmpty()) {
|
||||||
|
td = context.getTypeDeclaration(typeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td == null && mi.getExpression() instanceof SimpleName sn) {
|
||||||
|
String typeName = resolveLocalType(sn);
|
||||||
|
if (typeName != null) {
|
||||||
|
td = context.getTypeDeclaration(typeName);
|
||||||
|
if (td == null) {
|
||||||
|
CompilationUnit cu = (CompilationUnit) mi.getRoot();
|
||||||
|
td = context.getTypeDeclaration(typeName, cu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td == null) {
|
||||||
|
td = findEnclosingType(mi);
|
||||||
|
}
|
||||||
|
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||||
if (md != null) {
|
if (md != null) {
|
||||||
@@ -133,20 +158,42 @@ public class ConstantResolver {
|
|||||||
if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null;
|
if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null;
|
||||||
|
|
||||||
java.util.Map<String, String> localVars = new java.util.HashMap<>();
|
java.util.Map<String, String> localVars = new java.util.HashMap<>();
|
||||||
|
java.util.Set<String> declaredLocals = new java.util.HashSet<>();
|
||||||
|
|
||||||
for (int i = 0; i < mi.arguments().size(); i++) {
|
for (int i = 0; i < mi.arguments().size(); i++) {
|
||||||
Expression arg = (Expression) mi.arguments().get(i);
|
Expression arg = (Expression) mi.arguments().get(i);
|
||||||
String val = resolveInternal(arg, context, visited);
|
String val = resolveInternal(arg, context, visited);
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) md.parameters().get(i);
|
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) md.parameters().get(i);
|
||||||
localVars.put(param.getName().getIdentifier(), val);
|
String paramName = param.getName().getIdentifier();
|
||||||
|
localVars.put(paramName, val);
|
||||||
|
declaredLocals.add(paramName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localVars.isEmpty()) return null;
|
String[] finalResult = new String[1];
|
||||||
|
|
||||||
for (Object stmtObj : md.getBody().statements()) {
|
md.getBody().accept(new ASTVisitor() {
|
||||||
if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
@Override
|
||||||
if (es.getExpression() instanceof Assignment assignment) {
|
public boolean visit(VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragmentObj : vds.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
String varName = fragment.getName().getIdentifier();
|
||||||
|
declaredLocals.add(varName);
|
||||||
|
if (fragment.getInitializer() != null) {
|
||||||
|
String rhsVal = resolveExpressionWithParams(fragment.getInitializer(), localVars);
|
||||||
|
if (rhsVal == null) rhsVal = resolveInternal(fragment.getInitializer(), context, visited);
|
||||||
|
if (rhsVal != null) {
|
||||||
|
localVars.put(varName, rhsVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(vds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment assignment) {
|
||||||
Expression lhs = assignment.getLeftHandSide();
|
Expression lhs = assignment.getLeftHandSide();
|
||||||
String varName = null;
|
String varName = null;
|
||||||
if (lhs instanceof SimpleName sn) {
|
if (lhs instanceof SimpleName sn) {
|
||||||
@@ -159,28 +206,48 @@ public class ConstantResolver {
|
|||||||
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
|
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||||
|
|
||||||
if (varName != null && rhsVal != null) {
|
if (varName != null && rhsVal != null) {
|
||||||
localVars.put(varName, rhsVal);
|
|
||||||
if (varName.startsWith("this.")) {
|
if (varName.startsWith("this.")) {
|
||||||
localVars.put(varName.substring(5), rhsVal); // alias without 'this.'
|
localVars.put(varName, rhsVal);
|
||||||
|
String bareName = varName.substring(5);
|
||||||
|
if (!declaredLocals.contains(bareName)) {
|
||||||
|
localVars.put(bareName, rhsVal); // alias without 'this.' only if not shadowed
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
localVars.put("this." + varName, rhsVal); // alias with 'this.'
|
localVars.put(varName, rhsVal);
|
||||||
|
if (!declaredLocals.contains(varName)) {
|
||||||
|
localVars.put("this." + varName, rhsVal); // alias with 'this.' only if not shadowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchStatement ss) {
|
return super.visit(assignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(org.eclipse.jdt.core.dom.SwitchStatement ss) {
|
||||||
|
if (finalResult[0] == null) {
|
||||||
String result = evaluateSwitchStatement(ss, localVars, context, visited);
|
String result = evaluateSwitchStatement(ss, localVars, context, visited);
|
||||||
if (result != null) return result;
|
if (result != null) finalResult[0] = result;
|
||||||
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
}
|
||||||
|
return false; // Do not visit children — ReturnStatements inside the switch are handled by evaluateSwitchStatement
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||||
|
if (finalResult[0] == null) {
|
||||||
if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
|
if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
|
||||||
String result = evaluateSwitchExpression(se, localVars, context, visited);
|
String result = evaluateSwitchExpression(se, localVars, context, visited);
|
||||||
if (result != null) return result;
|
if (result != null) finalResult[0] = result;
|
||||||
} else {
|
} else if (rs.getExpression() != null) {
|
||||||
String result = resolveExpressionWithParams(rs.getExpression(), localVars);
|
String result = resolveExpressionWithParams(rs.getExpression(), localVars);
|
||||||
if (result != null) return result;
|
if (result == null) result = resolveInternal(rs.getExpression(), context, visited);
|
||||||
|
if (result != null) finalResult[0] = result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return super.visit(rs);
|
||||||
}
|
}
|
||||||
return null;
|
});
|
||||||
|
|
||||||
|
return finalResult[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
|
private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
|
||||||
@@ -251,7 +318,7 @@ public class ConstantResolver {
|
|||||||
if (expr instanceof SimpleName sn) {
|
if (expr instanceof SimpleName sn) {
|
||||||
String name = sn.getIdentifier();
|
String name = sn.getIdentifier();
|
||||||
if (paramValues.containsKey(name)) return paramValues.get(name);
|
if (paramValues.containsKey(name)) return paramValues.get(name);
|
||||||
return name;
|
return null;
|
||||||
} else if (expr instanceof FieldAccess fa) {
|
} else if (expr instanceof FieldAccess fa) {
|
||||||
String name = "this." + fa.getName().getIdentifier();
|
String name = "this." + fa.getName().getIdentifier();
|
||||||
if (paramValues.containsKey(name)) return paramValues.get(name);
|
if (paramValues.containsKey(name)) return paramValues.get(name);
|
||||||
@@ -410,4 +477,49 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
return (TypeDeclaration) parent;
|
return (TypeDeclaration) parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null) {
|
||||||
|
if (parent instanceof MethodDeclaration md) {
|
||||||
|
return md;
|
||||||
|
}
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveLocalType(SimpleName sn) {
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
|
||||||
|
if (enclosingMethod != null && enclosingMethod.getBody() != null) {
|
||||||
|
String[] typeName = new String[1];
|
||||||
|
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
typeName[0] = node.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (typeName[0] != null) return typeName[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(sn);
|
||||||
|
if (enclosingType != null) {
|
||||||
|
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||||
|
for (Object fragObj : field.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
return field.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback for complex chained expressions
|
// Fallback for complex chained expressions
|
||||||
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "";
|
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "this";
|
||||||
if (!exprStr.contains("(")) {
|
if (!exprStr.contains("(")) {
|
||||||
varName = exprStr;
|
varName = exprStr;
|
||||||
}
|
}
|
||||||
@@ -277,10 +277,13 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
List<org.eclipse.jdt.core.dom.Expression> tracedSetters = traceVariableAll(localSetterExpr);
|
List<org.eclipse.jdt.core.dom.Expression> tracedSetters = traceVariableAll(localSetterExpr);
|
||||||
for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) {
|
for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) {
|
||||||
extractConstantsFromExpression(tracedSetter, polymorphicEvents);
|
extractConstantsFromExpression(tracedSetter, polymorphicEvents);
|
||||||
|
if (tracedSetter instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic && cic.getAnonymousClassDeclaration() != null) {
|
||||||
|
resolvedValue = cic.toString() + "." + methodName + "()";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!polymorphicEvents.isEmpty()) {
|
if (!polymorphicEvents.isEmpty()) {
|
||||||
return TriggerPoint.builder()
|
return TriggerPoint.builder()
|
||||||
.event(tp.getEvent())
|
.event(resolvedValue)
|
||||||
.className(tp.getClassName())
|
.className(tp.getClassName())
|
||||||
.methodName(tp.getMethodName())
|
.methodName(tp.getMethodName())
|
||||||
.sourceFile(tp.getSourceFile())
|
.sourceFile(tp.getSourceFile())
|
||||||
@@ -295,6 +298,66 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (varName != null && declaredType == null) {
|
if (varName != null && declaredType == null) {
|
||||||
|
if (!varName.isEmpty() && !"this".equals(varName) && !"super".equals(varName)) {
|
||||||
|
TypeDeclaration currentTd = context.getTypeDeclaration(tp.getClassName());
|
||||||
|
if (currentTd != null) {
|
||||||
|
MethodDeclaration currentMd = context.findMethodDeclaration(currentTd, tp.getMethodName(), true);
|
||||||
|
if (currentMd != null) {
|
||||||
|
final String[] extractedFinalTraced = {null, null};
|
||||||
|
final String targetVar = varName;
|
||||||
|
final String mName = methodName;
|
||||||
|
final String[] finalDeclaredType = {null};
|
||||||
|
final String[] finalSourceMethod = {null};
|
||||||
|
currentMd.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||||
|
public boolean visit(org.eclipse.jdt.core.dom.VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(targetVar)) {
|
||||||
|
if (vdf.getInitializer() != null) {
|
||||||
|
org.eclipse.jdt.core.dom.Expression valueNode = traceVariable(vdf.getInitializer());
|
||||||
|
if (valueNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
|
||||||
|
if (cic.getAnonymousClassDeclaration() != null) {
|
||||||
|
extractedFinalTraced[0] = cic.toString();
|
||||||
|
extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
|
||||||
|
} else {
|
||||||
|
finalDeclaredType[0] = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
finalSourceMethod[0] = "inline-instantiation";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extractedFinalTraced[0] = valueNode.toString();
|
||||||
|
extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (finalDeclaredType[0] != null) {
|
||||||
|
declaredType = finalDeclaredType[0];
|
||||||
|
sourceMethod = finalSourceMethod[0];
|
||||||
|
}
|
||||||
|
if (extractedFinalTraced[0] != null) {
|
||||||
|
resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1];
|
||||||
|
System.out.println("DEBUG localized tracing resolvedValue to: " + resolvedValue);
|
||||||
|
// Since we updated resolvedValue, we need to extract from it if it's an inline anonymous class
|
||||||
|
if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) {
|
||||||
|
org.eclipse.jdt.core.dom.ASTParser p = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17);
|
||||||
|
p.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
|
||||||
|
p.setSource(resolvedValue.toCharArray());
|
||||||
|
org.eclipse.jdt.core.dom.ASTNode newNode = p.createAST(null);
|
||||||
|
if (newNode instanceof org.eclipse.jdt.core.dom.Expression) {
|
||||||
|
List<org.eclipse.jdt.core.dom.Expression> traced = traceVariableAll((org.eclipse.jdt.core.dom.Expression) newNode);
|
||||||
|
for (org.eclipse.jdt.core.dom.Expression ex : traced) {
|
||||||
|
extractConstantsFromExpression(ex, polymorphicEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (declaredType == null) {
|
||||||
for (String methodFqn : path) {
|
for (String methodFqn : path) {
|
||||||
declaredType = getVariableDeclaredType(methodFqn, varName);
|
declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||||
if (declaredType != null) {
|
if (declaredType != null) {
|
||||||
@@ -322,6 +385,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (declaredType == null && methodName != null && !methodName.equals("VariableReference")) {
|
if (declaredType == null && methodName != null && !methodName.equals("VariableReference")) {
|
||||||
System.out.println("DEBUG fallback triggered for methodName: " + methodName);
|
System.out.println("DEBUG fallback triggered for methodName: " + methodName);
|
||||||
|
|||||||
@@ -265,8 +265,26 @@ public class GenericEventDetector {
|
|||||||
return getSimpleNameString(right);
|
return getSimpleNameString(right);
|
||||||
}
|
}
|
||||||
} else if (expr instanceof MethodInvocation mi) {
|
} else if (expr instanceof MethodInvocation mi) {
|
||||||
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
|
String methodName = mi.getName().getIdentifier();
|
||||||
return getSimpleNameString((Expression) mi.arguments().get(0));
|
if (("equals".equals(methodName) || "equalsIgnoreCase".equals(methodName)) && !mi.arguments().isEmpty()) {
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
|
||||||
|
// If receiver is null (e.g., implicit this), fall back to arg
|
||||||
|
if (receiver == null) {
|
||||||
|
return getSimpleNameString(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize the one that looks like a constant (QualifiedName or StringLiteral)
|
||||||
|
if (receiver instanceof QualifiedName || receiver instanceof StringLiteral || receiver instanceof FieldAccess) {
|
||||||
|
return getSimpleNameString(receiver);
|
||||||
|
}
|
||||||
|
if (arg instanceof QualifiedName || arg instanceof StringLiteral || arg instanceof FieldAccess) {
|
||||||
|
return getSimpleNameString(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to receiver
|
||||||
|
return getSimpleNameString(receiver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -158,6 +158,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||||
|
Expression unwrappedBuilder = unwrapMessageBuilder(expr);
|
||||||
|
if (unwrappedBuilder != expr) {
|
||||||
|
expr = unwrappedBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
Expression tracedExpr = traceVariable(expr);
|
Expression tracedExpr = traceVariable(expr);
|
||||||
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
||||||
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
||||||
@@ -369,4 +374,22 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Expression unwrapMessageBuilder(Expression expr) {
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
MethodInvocation current = mi;
|
||||||
|
while (current != null) {
|
||||||
|
String name = current.getName().getIdentifier();
|
||||||
|
if (("withPayload".equals(name) || "just".equals(name)) && !current.arguments().isEmpty()) {
|
||||||
|
return (Expression) current.arguments().get(0);
|
||||||
|
}
|
||||||
|
Expression receiver = current.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation nextMi) {
|
||||||
|
current = nextMi;
|
||||||
|
} else {
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,4 +182,304 @@ public class ConstantResolverTest {
|
|||||||
|
|
||||||
assertThat(result).isEqualTo("PAYMENT_SUCCESS");
|
assertThat(result).isEqualTo("PAYMENT_SUCCESS");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodEvaluationWithNestedBlocksAndVars(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String process(String event) {\n" +
|
||||||
|
" String myVar = event;\n" +
|
||||||
|
" if (myVar != null) {\n" +
|
||||||
|
" return myVar;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" return \"DEFAULT\";\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.process(\"MY_EVENT\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process("MY_EVENT")
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("MY_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodShadowingReturnsParameter(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String a = \"OLD_FIELD\";\n" +
|
||||||
|
" public String process(String a) {\n" + // shadowing
|
||||||
|
" this.a = \"NEW_FIELD\";\n" +
|
||||||
|
" return a;\n" + // should return the parameter
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.process(\"PARAM_VALUE\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("PARAM_VALUE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodShadowingReturnsField(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String a = \"OLD_FIELD\";\n" +
|
||||||
|
" public String process(String a) {\n" + // shadowing
|
||||||
|
" this.a = \"NEW_FIELD_VALUE\";\n" +
|
||||||
|
" return this.a;\n" + // should return the field
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.process(\"PARAM_VALUE\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("NEW_FIELD_VALUE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodImplicitFieldAssignment(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String a = \"OLD_FIELD\";\n" +
|
||||||
|
" public String process(String p) {\n" +
|
||||||
|
" a = p;\n" + // implicit field assignment
|
||||||
|
" return this.a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.process(\"MY_EVENT\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("MY_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testZeroArgMethodEvaluation(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String a = \"MY_CONSTANT\";\n" +
|
||||||
|
" public String getEvent() {\n" +
|
||||||
|
" return this.a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.getEvent();\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.getEvent()
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("MY_CONSTANT");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a switch method is called with a <em>known</em> constant argument, {@code evaluateMethodOutput}
|
||||||
|
* should evaluate the switch and return only the matching branch.
|
||||||
|
*
|
||||||
|
* <p>Regression guard: before the {@code SwitchStatement.visit} fix, the ASTVisitor would also descend
|
||||||
|
* into child {@code ReturnStatement} nodes after the switch was already handled, potentially overwriting
|
||||||
|
* a correct result with a wrong one.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testOldStyleSwitchResolvesCorrectBranchForKnownConstant(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Classifier.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Classifier {\n" +
|
||||||
|
" public OrderEvents classify(String q) {\n" +
|
||||||
|
" switch (q) {\n" +
|
||||||
|
" case \"a\": return OrderEvents.A8;\n" +
|
||||||
|
" case \"b\": return OrderEvents.B2;\n" +
|
||||||
|
" default: return OrderEvents.DEF;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}\n" +
|
||||||
|
"enum OrderEvents { A8, B2, DEF }");
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Classifier c = new Classifier();\n" +
|
||||||
|
" OrderEvents r = c.classify(\"a\");\n" + // known constant → should match case "a"
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0];
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify("a")
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
// Only the matching branch should be returned, not all branches
|
||||||
|
assertThat(result).isEqualTo("OrderEvents.A8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a switch method is called with a <em>runtime</em> variable that cannot be statically resolved,
|
||||||
|
* {@code evaluateMethodOutput} must return {@code null} so the resolver falls back to the return-type
|
||||||
|
* enum set (covering all branches).
|
||||||
|
*
|
||||||
|
* <p>Regression: before the fix, {@code SwitchStatement.visit} returned {@code super.visit(ss)} (true),
|
||||||
|
* causing child {@code ReturnStatement} nodes inside the switch to fire. The first return — e.g.
|
||||||
|
* {@code return OrderEvents.A8} — was captured as if it were the only possible result, silently
|
||||||
|
* dropping the other branches. After the fix the visitor returns {@code false}, preventing child
|
||||||
|
* traversal, so the resolver correctly falls back to the full ENUM_SET.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testOldStyleSwitchWithUnknownVariableReturnsFallbackEnumSet(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Classifier.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Classifier {\n" +
|
||||||
|
" public OrderEvents classify(String q) {\n" +
|
||||||
|
" switch (q) {\n" +
|
||||||
|
" case \"a\": return OrderEvents.A8;\n" +
|
||||||
|
" case \"b\": return OrderEvents.B2;\n" +
|
||||||
|
" default: return OrderEvents.DEF;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}\n" +
|
||||||
|
"enum OrderEvents { A8, B2, DEF }");
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call(String x) {\n" + // x is a runtime parameter — not a constant
|
||||||
|
" Classifier c = new Classifier();\n" +
|
||||||
|
" OrderEvents r = c.classify(x);\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0];
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify(x)
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
// Must cover ALL branches via ENUM_SET — not just the first one ("A8")
|
||||||
|
assertThat(result).startsWith("ENUM_SET:");
|
||||||
|
assertThat(result).contains("OrderEvents.A8");
|
||||||
|
assertThat(result).contains("OrderEvents.B2");
|
||||||
|
assertThat(result).contains("OrderEvents.DEF");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ public class GenericEventDetectorTest {
|
|||||||
"import org.springframework.statemachine.StateMachine;\n" +
|
"import org.springframework.statemachine.StateMachine;\n" +
|
||||||
"public class MyService {\n" +
|
"public class MyService {\n" +
|
||||||
" private StateMachine<String, String> stateMachine;\n" +
|
" private StateMachine<String, String> stateMachine;\n" +
|
||||||
" public MyEnum getEvent() { return MyEnum.STATE_A; }\n" + // Pretend it returns the enum
|
" public MyEnum getEvent() { return someExternalCall(); }\n" +
|
||||||
|
" public MyEnum someExternalCall() { return null; }\n" +
|
||||||
" public void trigger() {\n" +
|
" public void trigger() {\n" +
|
||||||
" stateMachine.sendEvent(this.getEvent());\n" +
|
" stateMachine.sendEvent(this.getEvent());\n" +
|
||||||
" }\n" +
|
" }\n" +
|
||||||
|
|||||||
@@ -1002,9 +1002,10 @@ class HeuristicCallGraphEngineTest {
|
|||||||
|
|
||||||
assertThat(chains).hasSize(1);
|
assertThat(chains).hasSize(1);
|
||||||
CallChain chain = chains.get(0);
|
CallChain chain = chains.get(0);
|
||||||
// It successfully traces the local variable 'event' to its anonymous class initializer
|
// The engine traces the anonymous class's getType() through the RichEvent interface,
|
||||||
assertThat(chain.getTriggerPoint().getEvent()).startsWith("new RichEvent(){");
|
// resolving to OrderEvents.CREATE via the enum return-type fallback.
|
||||||
assertThat(chain.getTriggerPoint().getEvent()).endsWith(".getType()");
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.CREATE");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -2753,4 +2754,80 @@ class HeuristicCallGraphEngineTest {
|
|||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
.containsExactlyInAnyOrder("MyEvents.A", "MyEvents.B");
|
.containsExactlyInAnyOrder("MyEvents.A", "MyEvents.B");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression test for the early-exit bug in {@code resolveTriggerPointParameters}.
|
||||||
|
*
|
||||||
|
* <p>When {@code traceLocalSetter} matches a field method call whose name happens to equal the
|
||||||
|
* accessor suffix (e.g. {@code mapper.transform(dto)} matching {@code methodName="transform"}),
|
||||||
|
* and the argument passed to that method is a non-constant runtime parameter, no events are
|
||||||
|
* extracted. The old condition
|
||||||
|
* <pre>if (!polymorphicEvents.isEmpty() || !resolvedValue.equals(tp.getEvent()))</pre>
|
||||||
|
* caused an early return with empty {@code polymorphicEvents} because {@code resolvedValue} had
|
||||||
|
* already diverged from the original event name.
|
||||||
|
*
|
||||||
|
* <p>After the fix the condition is just {@code !polymorphicEvents.isEmpty()}, so resolution
|
||||||
|
* continues through {@code getVariableDeclaredType} → {@code resolveMethodReturnConstant},
|
||||||
|
* which correctly traces the field's declared type to its implementation and extracts the
|
||||||
|
* constant returned by {@code TransformerImpl.transform()}.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventsWhenFieldTransformerMethodMatchesSetterPatternButArgIsNonConstant() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private Transformer transformer;
|
||||||
|
public void processEvent(String dto) {
|
||||||
|
Event e = transformer.transform(dto);
|
||||||
|
machine.fire(e.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Transformer {
|
||||||
|
Event transform(String input);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransformerImpl implements Transformer {
|
||||||
|
public Event transform(String input) {
|
||||||
|
return new Event("ORDER_DISPATCHED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Event {
|
||||||
|
private String type;
|
||||||
|
public Event(String type) { this.type = type; }
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Machine {
|
||||||
|
public void fire(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_transformer");
|
||||||
|
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.OrderController")
|
||||||
|
.methodName("processEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.Machine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
// Must resolve through TransformerImpl.transform() → "ORDER_DISPATCHED",
|
||||||
|
// NOT return empty polymorphicEvents due to early exit on the setter match.
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("ORDER_DISPATCHED");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user