19 Commits

Author SHA1 Message Date
bc232dfd2e heuristics updated consolidation 2026-06-25 00:18:46 +02:00
5a78110dd9 refactor: implement phase 1 - unify parameter tracing in base class 2026-06-24 23:43:35 +02:00
ac2afca053 heuristics updated 2026-06-24 22:04:15 +02:00
c6db825807 test added 2026-06-24 22:03:57 +02:00
1b1e036680 te21 2026-06-24 18:25:36 +02:00
ae75379f77 te2 2026-06-24 18:03:19 +02:00
e2f8be9c6b te 2026-06-24 16:37:57 +02:00
cb97392bdd test 2026-06-24 06:57:46 +02:00
f30538e8c9 forward analysis nemotron3 ultra 2026-06-23 22:33:34 +02:00
131ccbeda0 forward analysis 2 2026-06-22 18:51:20 +02:00
14b311be2b forward analysis 2026-06-22 18:38:19 +02:00
1b690582d6 fixes 2 2026-06-22 14:11:35 +02:00
3ca834fa71 fixes 2026-06-22 13:53:27 +02:00
c6b4a4be1e better things extraction of constants 2 2026-06-22 13:12:48 +02:00
24a9be3a4e better things extraction of constants 2026-06-22 07:30:05 +02:00
07d241a060 better things 2026-06-21 22:55:39 +02:00
32de0a51c6 edge case fixes 2026-06-21 18:45:24 +02:00
cbb00e8a0f Fix AST constructor tracking gap for variables and fields 2026-06-21 18:36:37 +02:00
6596303b7d Replace fallback string scraper with precise AST traversal 2026-06-21 18:20:30 +02:00
29 changed files with 4392 additions and 468 deletions

View File

@@ -27,7 +27,7 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
hasPolyMatch = true;
break;
}
continue; // Stricter matching: do not fallback if both have qualifiers but don't match
continue;
}
String simplePe = pe;

View File

@@ -0,0 +1,69 @@
package click.kamil.springstatemachineexporter.analysis.enricher.path;
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class ForwardPathEstimationEnricher implements AnalysisEnricher {
private final boolean isEnabled;
private final PathValueEstimator estimator;
public ForwardPathEstimationEnricher() {
this.isEnabled = Boolean.parseBoolean(System.getProperty("analyzer.forward-path-estimation.enabled", "false"));
this.estimator = new SymbolicPathValueEstimator();
}
@Override
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
if (!isEnabled || result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
return;
}
log.info("Running Symbolic Forward Path Analysis for {}", result.getName());
List<CallChain> chains = result.getMetadata().getCallChains();
List<CallChain> updatedChains = chains.stream().map(chain -> {
if (chain.getTriggerPoint() == null || chain.getTriggerPoint().getEvent() == null) return chain;
// If the event looks like a dynamic variable (no dots, not an enum format)
if (!chain.getTriggerPoint().getEvent().contains(".")) {
List<String> estimatedValues = estimator.estimatePossibleValues(chain, context);
if (estimatedValues != null && !estimatedValues.isEmpty()) {
log.info("Symbolic Path Analysis estimated values for {} -> {}", chain.getTriggerPoint().getEvent(), estimatedValues);
List<String> polyEvents = new ArrayList<>();
if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
polyEvents.addAll(chain.getTriggerPoint().getPolymorphicEvents());
}
polyEvents.addAll(estimatedValues);
TriggerPoint newTp = chain.getTriggerPoint().toBuilder()
.polymorphicEvents(polyEvents)
.build();
return chain.toBuilder().triggerPoint(newTp).build();
}
}
return chain;
}).collect(Collectors.toList());
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata updatedMetadata =
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
.triggers(result.getMetadata().getTriggers())
.entryPoints(result.getMetadata().getEntryPoints())
.callChains(updatedChains)
.properties(result.getMetadata().getProperties())
.build();
result.setMetadata(updatedMetadata);
}
}

View File

@@ -0,0 +1,10 @@
package click.kamil.springstatemachineexporter.analysis.enricher.path;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.List;
public interface PathValueEstimator {
List<String> estimatePossibleValues(CallChain chain, CodebaseContext context);
}

View File

@@ -0,0 +1,130 @@
package click.kamil.springstatemachineexporter.analysis.enricher.path;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SymbolicPathValueEstimator implements PathValueEstimator {
@Override
public List<String> estimatePossibleValues(CallChain chain, CodebaseContext context) {
if (chain.getMethodChain() == null || chain.getMethodChain().isEmpty()) {
return null;
}
Set<String> collectedConstants = new HashSet<>();
for (String methodFqn : chain.getMethodChain()) {
int lastDot = methodFqn.lastIndexOf('.');
if (lastDot == -1) continue;
String className = methodFqn.substring(0, lastDot);
String methodName = methodFqn.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) continue;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null || md.getBody() == null) continue;
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if ((node.getName().getIdentifier().equals("equals") || node.getName().getIdentifier().equals("equalsIgnoreCase"))
&& node.arguments().size() == 1) {
Expression caller = node.getExpression();
Expression arg = (Expression) node.arguments().get(0);
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
String callerVal = resolver.resolve(caller, context);
if (callerVal != null && !callerVal.isEmpty()) {
addValue(collectedConstants, callerVal);
}
String argVal = resolver.resolve(arg, context);
if (argVal != null && !argVal.isEmpty()) {
addValue(collectedConstants, argVal);
}
}
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
public boolean visit(SwitchStatement node) {
for (Object stmt : node.statements()) {
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
for (Object expr : sc.expressions()) {
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
String val = resolver.resolve((Expression) expr, context);
if (val != null && !val.isEmpty()) {
addValue(collectedConstants, val);
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SwitchExpression node) {
for (Object stmt : node.statements()) {
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
for (Object expr : sc.expressions()) {
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
String val = resolver.resolve((Expression) expr, context);
if (val != null && !val.isEmpty()) {
addValue(collectedConstants, val);
}
}
}
}
return super.visit(node);
}
});
}
if (collectedConstants.isEmpty()) return null;
return new ArrayList<>(collectedConstants);
}
private void addValue(Set<String> collectedConstants, String val) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
String parsed = parseEnumSetElement(eVal);
if (!collectedConstants.contains(parsed)) collectedConstants.add(parsed);
}
} else {
collectedConstants.add(val);
}
}
private String parseEnumSetElement(String element) {
if (element.contains(".")) {
return element.substring(element.lastIndexOf('.') + 1);
}
return element;
}
}

View File

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

View File

@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
import java.util.Map;
@Data
@Builder
@Builder(toBuilder = true)
@Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true)
public class TriggerPoint {

View File

@@ -14,6 +14,14 @@ public class ConstantResolver {
return resolveInternal(expr, context, new HashSet<>());
}
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context) {
return evaluateSwitchExpression(se, paramValues, context, new HashSet<>());
}
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context) {
return evaluateSwitchStatement(ss, paramValues, context, new HashSet<>());
}
private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) {
if (expr == null) return null;
@@ -64,6 +72,9 @@ public class ConstantResolver {
String val = resolveManual(qn, context, visited);
return val != null ? val : qn.toString();
}
if (expr instanceof FieldAccess fa) {
return resolveManual(fa.getName(), context, visited);
}
if (expr instanceof SimpleName sn) {
return resolveManual(sn, context, visited);
}
@@ -87,7 +98,14 @@ public class ConstantResolver {
return sn.getIdentifier();
}
}
if ("valueOf".equals(mi.getName().getIdentifier()) && mi.getExpression() != null) {
java.util.List<String> enumValues = context.getEnumValues(mi.getExpression().toString());
if (enumValues != null && !enumValues.isEmpty()) {
return "ENUM_SET:" + String.join(",", enumValues);
}
}
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
ITypeBinding returnType = mb.getReturnType();
@@ -98,13 +116,43 @@ public class ConstantResolver {
}
}
} 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) {
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
if (md != null && md.getReturnType2() != null) {
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
if (values != null && !values.isEmpty()) {
return "ENUM_SET:" + String.join(",", values);
if (md != null) {
String evaluated = evaluateMethodOutput(mi, md, context, visited);
if (evaluated != null) return evaluated;
if (md.getReturnType2() != null) {
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
if (values != null && !values.isEmpty()) {
return "ENUM_SET:" + String.join(",", values);
}
}
}
}
@@ -114,6 +162,239 @@ public class ConstantResolver {
return null;
}
private String evaluateMethodOutput(MethodInvocation mi, MethodDeclaration md, CodebaseContext context, Set<String> visited) {
if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null;
TypeDeclaration td = findEnclosingType(md);
if (td == null) return null;
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
if (!visited.add(methodFqn)) {
return null; // Cycle detected in method evaluation
}
try {
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++) {
Expression arg = (Expression) mi.arguments().get(i);
String val = resolveInternal(arg, context, visited);
if (val != null) {
org.eclipse.jdt.core.dom.SingleVariableDeclaration param =
(org.eclipse.jdt.core.dom.SingleVariableDeclaration) md.parameters().get(i);
String paramName = param.getName().getIdentifier();
localVars.put(paramName, val);
declaredLocals.add(paramName);
}
}
return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited);
} finally {
visited.remove(methodFqn);
}
}
/**
* Evaluates a method body with pre-supplied local values (e.g. field values derived
* from the specific {@code ClassInstanceCreation} that created the receiver object).
* Used by {@link click.kamil.springstatemachineexporter.analysis.service.AbstractCallGraphEngine}
* to resolve transforming getters — methods that map a constructor-injected enum to a
* different state-machine enum via switch expressions.
*/
public String evaluateMethodBodyWithLocals(MethodDeclaration md,
java.util.Map<String, String> prebuiltLocals,
CodebaseContext context,
Set<String> visited) {
if (md.getBody() == null) return null;
TypeDeclaration td = findEnclosingType(md);
if (td == null) return null;
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
if (!visited.add(methodFqn)) {
return null; // Cycle detected in method evaluation
}
try {
java.util.Map<String, String> localVars = new java.util.HashMap<>(prebuiltLocals);
java.util.Set<String> declaredLocals = new java.util.HashSet<>(prebuiltLocals.keySet());
return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited);
} finally {
visited.remove(methodFqn);
}
}
/**
* Core body-evaluation logic shared by {@link #evaluateMethodOutput} and
* {@link #evaluateMethodBodyWithLocals}. Visits the block, tracks assignments,
* evaluates switch statements/expressions, and captures the first resolved return value.
*/
private String evaluateBody(org.eclipse.jdt.core.dom.Block body,
java.util.Map<String, String> localVars,
java.util.Set<String> declaredLocals,
CodebaseContext context,
Set<String> visited) {
String[] finalResult = new String[1];
body.accept(new ASTVisitor() {
@Override
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();
String varName = null;
if (lhs instanceof SimpleName sn) {
varName = sn.getIdentifier();
} else if (lhs instanceof FieldAccess fa) {
varName = "this." + fa.getName().getIdentifier();
}
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars);
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
if (varName != null && rhsVal != null) {
if (varName.startsWith("this.")) {
localVars.put(varName, rhsVal);
String bareName = varName.substring(5);
if (!declaredLocals.contains(bareName)) localVars.put(bareName, rhsVal);
} else {
localVars.put(varName, rhsVal);
if (!declaredLocals.contains(varName)) localVars.put("this." + varName, rhsVal);
}
}
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);
if (result != null) finalResult[0] = result;
}
return false; // children handled inside 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) {
String result = evaluateSwitchExpression(se, localVars, context, visited);
if (result != null) finalResult[0] = result;
} else if (rs.getExpression() != null) {
String result = resolveExpressionWithParams(rs.getExpression(), localVars);
if (result == null) result = resolveInternal(rs.getExpression(), context, visited);
if (result != null) finalResult[0] = result;
}
}
return super.visit(rs);
}
});
return finalResult[0];
}
private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues);
if (switchVar == null) return null;
boolean matchingCase = false;
for (Object stmtObj : ss.statements()) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
matchingCase = false;
if (sc.isDefault()) {
matchingCase = true;
} else {
for (Object exprObj : sc.expressions()) {
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
caseVal = caseSn.getIdentifier();
}
if (caseVal != null && switchVar.endsWith(caseVal)) {
matchingCase = true;
break;
}
}
}
} else if (matchingCase) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
return resolveInternal(rs.getExpression(), context, visited);
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) {
return resolveInternal(ys.getExpression(), context, visited);
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
return resolveInternal(es.getExpression(), context, visited);
}
}
}
return null;
}
private String evaluateSwitchExpression(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues);
if (switchVar == null) return null;
boolean matchingCase = false;
for (Object stmtObj : se.statements()) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
matchingCase = false;
if (sc.isDefault()) {
matchingCase = true;
} else {
for (Object exprObj : sc.expressions()) {
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
caseVal = caseSn.getIdentifier();
}
if (caseVal != null && switchVar.endsWith(caseVal)) {
matchingCase = true;
break;
}
}
}
} else if (matchingCase) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
return resolveInternal(es.getExpression(), context, visited);
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) {
return resolveInternal(ys.getExpression(), context, visited);
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
return resolveInternal(rs.getExpression(), context, visited);
}
}
}
return null;
}
private String resolveExpressionWithParams(Expression expr, java.util.Map<String, String> paramValues) {
if (expr instanceof SimpleName sn) {
String name = sn.getIdentifier();
if (paramValues.containsKey(name)) return paramValues.get(name);
return null;
} else if (expr instanceof FieldAccess fa) {
String name = "this." + fa.getName().getIdentifier();
if (paramValues.containsKey(name)) return paramValues.get(name);
return null;
} else if (expr instanceof QualifiedName qn) {
return qn.getName().getIdentifier();
} else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) {
return sl.getLiteralValue();
}
return null;
}
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
@@ -193,6 +474,28 @@ public class ConstantResolver {
}
}
}
// Check constructors for assignments
for (MethodDeclaration md : td.getMethods()) {
if (md.isConstructor() && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ExpressionStatement es) {
if (es.getExpression() instanceof Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
boolean matches = false;
if (lhs instanceof SimpleName snLhs && snLhs.getIdentifier().equals(fieldName)) {
matches = true;
} else if (lhs instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName) && fa.getExpression() instanceof ThisExpression) {
matches = true;
}
if (matches) {
return resolveInternal(assignment.getRightHandSide(), context, visited);
}
}
}
}
}
}
} finally {
visited.remove(fieldId);
}
@@ -238,4 +541,49 @@ public class ConstantResolver {
}
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;
}
}

View File

@@ -265,8 +265,26 @@ public class GenericEventDetector {
return getSimpleNameString(right);
}
} else if (expr instanceof MethodInvocation mi) {
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
return getSimpleNameString((Expression) mi.arguments().get(0));
String methodName = mi.getName().getIdentifier();
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;

View File

@@ -35,7 +35,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
List<String> args = resolveArguments(node.arguments());
for (String calledMethod : calledMethods) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
}
for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) {
@@ -45,7 +46,14 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs, receiver));
}
}
} else {
@@ -58,11 +66,18 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
}
if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs, receiver));
List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver));
}
}
}
@@ -94,7 +109,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
calledMethod = superFqn + "." + methodName;
}
List<String> args = resolveArguments(node.arguments());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
String receiver = "super";
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
}
}
}
@@ -106,62 +122,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
return graph;
}
protected List<String> resolveArguments(List<?> astArguments) {
List<String> args = new ArrayList<>();
for (Object argObj : astArguments) {
Expression expr = (Expression) argObj;
// 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;
}
}
}
}
@Override
protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
// Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
// where we can substitute the actual receiver from the call edge
boolean isThisOrSuperGetter = originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi
&& (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression || mi.getExpression() instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation)
&& mi.arguments().isEmpty();
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, ...))
Expression tracedExpr = traceVariable(expr);
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
}
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
Expression firstArg = (Expression) cic.arguments().get(0);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg; // Only unwrap if it's actually a constant
}
}
String val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
}
args.add(val);
if (isThisOrSuperGetter) {
return originalExpr.toString(); // Preserve "this.getTransitionType()" for later substitution
}
return args;
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized
// from a ClassInstanceCreation (directly or via a factory method). This avoids
// the broad ENUM_SET fallback when the getter transforms one enum to another.
if (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation getterMi
&& getterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName instanceSn
&& getterMi.arguments().isEmpty()) {
String narrowed = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
if (narrowed != null) {
return narrowed;
}
}
return resolvedValue;
}
protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
@@ -175,6 +160,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
String definingClass = findDefiningClass(className, methodName);
if (definingClass != null && !definingClass.equals(className)) {
allResolved.set(0, definingClass + "." + methodName);
className = definingClass;
}
List<String> impls = context.getImplementations(className);
for (String impl : impls) {
allResolved.add(impl + "." + methodName);
@@ -184,6 +175,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
return allResolved;
}
private String findDefiningClass(String className, String methodName) {
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) return null;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
if (md != null) {
return className;
}
String superFqn = context.getSuperclassFqn(td);
while (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
MethodDeclaration superMd = context.findMethodDeclaration(superTd, methodName, false);
if (superMd != null) {
return superFqn;
}
superFqn = context.getSuperclassFqn(superTd);
} else {
break;
}
}
return null;
}
protected String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier();
@@ -308,4 +324,519 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
return null;
}
/**
* Attempts to narrow the resolution of {@code instanceVar.getterMethod()} when the
* instance variable is initialised from a {@link org.eclipse.jdt.core.dom.ClassInstanceCreation}
* (either directly or via a factory/mapper method).
*
* <p>This handles both:
* <ul>
* <li>Simple pass-through: {@code Wrapper(MyEnum e){this.e=e;} getE(){return e;}}</li>
* <li>Transformation: {@code getSmEvent(){return switch(dtoEnum){case A->SmEvents.X;…};}}</li>
* </ul>
*
* @return the resolved constant string, or {@code null} when narrowing is not possible
*/
private String tryNarrowGetterViaLocalVarChain(
MethodInvocation getterCall, SimpleName instanceSn) {
MethodDeclaration enclosing = findEnclosingMethod(getterCall);
if (enclosing == null || enclosing.getBody() == null) return null;
String varName = instanceSn.getIdentifier();
String getterName = getterCall.getName().getIdentifier();
// Find the declared type and initialiser of the local variable
String[] varTypeName = {null};
Expression[] initExpr = {null};
enclosing.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement vds) {
for (Object fragObj : vds.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
varTypeName[0] = vds.getType().toString();
initExpr[0] = frag.getInitializer();
}
}
return super.visit(vds);
}
});
if (varTypeName[0] == null) return null;
// Obtain the ClassInstanceCreation that created the object
org.eclipse.jdt.core.dom.ClassInstanceCreation cic = null;
if (initExpr[0] instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation directCic) {
cic = directCic;
} else if (initExpr[0] instanceof MethodInvocation factoryMi
&& factoryMi.getExpression() instanceof SimpleName receiverSn) {
// e.g. "mapper.toMsg(dto)" → find the CIC returned by the factory method
String receiverType = resolveReceiverTypeFallback(receiverSn);
if (receiverType != null) {
cic = findReturnedCIC(receiverType, factoryMi.getName().getIdentifier(), context);
}
}
if (cic == null) return null;
// Resolve the variable's declared type to a TypeDeclaration
CompilationUnit cu = (CompilationUnit) getterCall.getRoot();
TypeDeclaration varTypeTd = context.getTypeDeclaration(varTypeName[0], cu);
if (varTypeTd == null) varTypeTd = context.getTypeDeclaration(varTypeName[0]);
if (varTypeTd == null) return null;
// Evaluate the getter body with field values substituted from the CIC's constructor args
java.util.Map<String, String> fieldValues = buildFieldValuesFromCIC(varTypeTd, cic, context, this::evaluateMethodCallInConstructor);
if (fieldValues.isEmpty()) return null;
// Track setter calls on the variable between its initialization and the getter call
// to capture field updates like e.setType("NEW_VALUE")
trackSetterCallsBetween(enclosing.getBody(), varName, getterCall, fieldValues, context);
MethodDeclaration getter = context.findMethodDeclaration(varTypeTd, getterName, true);
if (getter == null || getter.getBody() == null) return null;
return constantResolver.evaluateMethodBodyWithLocals(
getter, fieldValues, context, new java.util.HashSet<>());
}
/**
* Scans the method body for setter calls or field assignments to the given variable
* that occur between the variable's initialization and the getter call.
* Updates fieldValues with any field values set via setters.
*/
private void trackSetterCallsBetween(org.eclipse.jdt.core.dom.Block methodBody, String varName,
org.eclipse.jdt.core.dom.MethodInvocation getterCall,
java.util.Map<String, String> fieldValues,
CodebaseContext context) {
java.util.List<org.eclipse.jdt.core.dom.ASTNode> statements = new java.util.ArrayList<>();
for (Object stmtObj : methodBody.statements()) {
statements.add((org.eclipse.jdt.core.dom.ASTNode) stmtObj);
}
int varDeclIndex = -1;
int getterCallIndex = -1;
for (int i = 0; i < statements.size(); i++) {
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
// Find variable declaration
if (varDeclIndex == -1) {
if (stmt instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement vds) {
for (Object fragObj : vds.fragments()) {
if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment frag
&& frag.getName().getIdentifier().equals(varName)) {
varDeclIndex = i;
break;
}
}
}
}
// Find getter call
if (getterCallIndex == -1) {
if (stmt.toString().contains(getterCall.toString())) {
getterCallIndex = i;
break;
}
}
}
if (varDeclIndex >= 0 && getterCallIndex > varDeclIndex) {
// Scan statements between var declaration and getter call
for (int i = varDeclIndex + 1; i < getterCallIndex; i++) {
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
org.eclipse.jdt.core.dom.Expression expr = es.getExpression();
// Check for setter calls: var.setField(value)
if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
&& sn.getIdentifier().equals(varName)) {
String methodName = mi.getName().getIdentifier();
// Check if it's a setter (setXxx or xxx)
if (methodName.startsWith("set") && methodName.length() > 3) {
String fieldName = Character.toLowerCase(methodName.charAt(3))
+ methodName.substring(4);
if (!mi.arguments().isEmpty()) {
org.eclipse.jdt.core.dom.Expression arg = (org.eclipse.jdt.core.dom.Expression) mi.arguments().get(0);
String val = constantResolver.resolve(arg, context);
if (val != null) {
fieldValues.put(fieldName, val);
fieldValues.put("this." + fieldName, val);
}
}
}
}
}
// Check for direct field assignments: var.field = value
else if (expr instanceof org.eclipse.jdt.core.dom.Assignment assignment) {
if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
&& sn.getIdentifier().equals(varName)) {
String fieldName = fa.getName().getIdentifier();
org.eclipse.jdt.core.dom.Expression rhs = assignment.getRightHandSide();
String val = constantResolver.resolve(rhs, context);
if (val != null) {
fieldValues.put(fieldName, val);
fieldValues.put("this." + fieldName, val);
}
}
}
}
}
}
}
}
/**
* Extracts a string representation of the receiver expression from a MethodInvocation.
* Handles ThisExpression, SimpleName, and other expression types.
*/
protected String getReceiverString(Expression receiver) {
if (receiver == null) {
return null;
}
if (receiver instanceof ThisExpression) {
return "this";
}
if (receiver instanceof SuperMethodInvocation) {
return "super";
}
if (receiver instanceof SimpleName sn) {
return sn.getIdentifier();
}
return receiver.toString();
}
/**
* Finds the receiver expression used in the caller method when calling the target method.
* Walks the caller's AST to find a MethodInvocation with the target method name
* and returns the receiver string.
*/
protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
int lastDot = callerFqn.lastIndexOf('.');
if (lastDot < 0) return null;
String callerClass = callerFqn.substring(0, lastDot);
String callerMethod = callerFqn.substring(lastDot + 1);
int targetLastDot = targetMethodFqn.lastIndexOf('.');
if (targetLastDot < 0) return null;
String targetMethodName = targetMethodFqn.substring(targetLastDot + 1);
TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
if (callerTd == null) return null;
MethodDeclaration callerMd = context.findMethodDeclaration(callerTd, callerMethod, false);
if (callerMd == null || callerMd.getBody() == null) return null;
final String[] foundReceiver = {null};
callerMd.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if (foundReceiver[0] != null) return false;
String called = resolveCalledMethod(node);
if (called != null && (called.equals(targetMethodFqn) || called.endsWith("." + targetMethodName))) {
foundReceiver[0] = getReceiverString(node.getExpression());
return false;
}
return super.visit(node);
}
});
return foundReceiver[0];
}
@Override
protected TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
if (path.size() < 2) return tp;
String event = tp.getEvent();
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
String currentParamName = event;
String resolvedValue = event;
String methodSuffix = "";
boolean didThisSuperSubstitution = false;
String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix);
currentParamName = extractedEntry[0];
methodSuffix = extractedEntry[1];
for (int i = path.size() - 1; i > 0; i--) {
String target = path.get(i);
String caller = path.get(i - 1);
int paramIndex = getParameterIndex(target, currentParamName);
if (paramIndex < 0) {
java.util.Map<String, String> parameterValues = buildParameterValuesMap(caller, target, callGraph, path, i);
String tracedVar = traceLocalVariable(target, currentParamName, parameterValues);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix);
tracedVar = extractedTraced[0];
methodSuffix = extractedTraced[1];
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
paramIndex = getParameterIndex(target, currentParamName);
}
}
if (paramIndex < 0) {
break;
}
List<CallEdge> edges = callGraph.get(caller);
boolean found = false;
if (edges != null) {
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
if (paramIndex < edge.getArguments().size()) {
String arg = edge.getArguments().get(paramIndex);
if (arg != null) {
System.out.println("DEBUG TRACE: caller=" + caller + " target=" + target + " paramIndex=" + paramIndex + " arg=" + arg + " receiver=" + edge.getReceiver());
String receiver = edge.getReceiver();
if (receiver != null && !receiver.isEmpty() && ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super."))) {
String actualReceiver = receiver;
boolean needPrevReceiver = "this".equals(arg) || arg.startsWith("this.");
if (needPrevReceiver && !"this".equals(receiver) && !"super".equals(receiver)) {
if (i - 1 > 0) {
String prevCaller = path.get(i - 2);
List<CallEdge> prevEdges = callGraph.get(prevCaller);
if (prevEdges != null) {
for (CallEdge prevEdge : prevEdges) {
if (prevEdge.getTargetMethod().equals(caller) || isHeuristicMatch(prevEdge.getTargetMethod(), caller)) {
actualReceiver = prevEdge.getReceiver();
System.out.println("DEBUG TRACE: found prev edge, actualReceiver=" + actualReceiver);
break;
}
}
}
}
} else if ("this".equals(receiver) || "super".equals(receiver)) {
if (i - 1 > 0) {
String prevCaller = path.get(i - 2);
List<CallEdge> prevEdges = callGraph.get(prevCaller);
if (prevEdges != null) {
for (CallEdge prevEdge : prevEdges) {
if (prevEdge.getTargetMethod().equals(caller) || isHeuristicMatch(prevEdge.getTargetMethod(), caller)) {
actualReceiver = prevEdge.getReceiver();
System.out.println("DEBUG TRACE: found prev edge, actualReceiver=" + actualReceiver);
break;
}
}
}
}
}
if (actualReceiver != null && !actualReceiver.isEmpty()) {
if ("this".equals(arg) || arg.startsWith("this.")) {
arg = arg.replaceFirst("^this", actualReceiver);
didThisSuperSubstitution = true;
} else if ("super".equals(arg) || arg.startsWith("super.")) {
arg = arg.replaceFirst("^super", actualReceiver);
didThisSuperSubstitution = true;
}
System.out.println("DEBUG TRACE: after substitution arg=" + arg);
}
}
String[] extractedArg = extractMethodSuffix(arg, methodSuffix);
arg = extractedArg[0];
methodSuffix = extractedArg[1];
currentParamName = arg;
resolvedValue = arg + methodSuffix;
found = true;
break;
}
}
}
}
}
if (!found) break;
}
String entryMethod = path.get(0);
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
if (resolvedValue != null && resolvedValue.contains(".") && !resolvedValue.contains("(") && !resolvedValue.contains(" ")) {
TriggerPoint modifiedTp = TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.build();
return super.resolveTriggerPointParameters(modifiedTp, path, callGraph);
}
if (entryParamIndex < 0) {
if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) {
String localMethodName = methodSuffix.substring(1).replace("()", "");
org.eclipse.jdt.core.dom.Expression localSetterExpr = traceLocalSetter(entryMethod, currentParamName, localMethodName);
if (localSetterExpr != null) {
List<String> setterEvents = new ArrayList<>();
List<org.eclipse.jdt.core.dom.Expression> tracedSetters = traceVariableAll(localSetterExpr);
for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) {
extractConstantsFromExpression(tracedSetter, setterEvents);
}
if (!setterEvents.isEmpty()) {
return TriggerPoint.builder()
.event(tp.getEvent())
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(setterEvents)
.build();
}
}
}
boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null
&& !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new ");
if (!hasGetterOnReceiver) {
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
tracedVar = extractedFinalTraced[0];
methodSuffix = extractedFinalTraced[1];
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
}
}
}
String finalResolvedValue = resolvedValue;
boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null
&& !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new ");
if (hasGetterOnReceiver && didThisSuperSubstitution) {
List<String> getterEvents = evaluateGetterOnReceiver(finalResolvedValue, methodSuffix, entryMethod, path, callGraph);
if (getterEvents != null && !getterEvents.isEmpty()) {
return TriggerPoint.builder()
.event(tp.getEvent())
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(getterEvents)
.build();
}
// If we couldn't evaluate the getter, fall back to super with modified trigger point
TriggerPoint modifiedTp = TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.build();
return super.resolveTriggerPointParameters(modifiedTp, path, callGraph);
}
return super.resolveTriggerPointParameters(tp, path, callGraph);
}
private List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
System.out.println("DEBUG evaluateGetterOnReceiver: resolvedValue=" + resolvedValue + " methodSuffix=" + methodSuffix + " entryMethod=" + entryMethod);
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17);
exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
exprParser.setSource(resolvedValue.toCharArray());
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null);
if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
String getterName = mi.getName().getIdentifier();
org.eclipse.jdt.core.dom.Expression receiver = mi.getExpression();
String receiverName = null;
String receiverType = null;
if (receiver instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
receiverName = sn.getIdentifier();
receiverType = getVariableDeclaredType(entryMethod, receiverName);
System.out.println("DEBUG evaluateGetterOnReceiver: receiverName=" + receiverName + " receiverType=" + receiverType);
}
if (receiverType == null && path.size() > 1) {
String callerMethod = path.get(1);
receiverType = getVariableDeclaredType(callerMethod, receiverName);
System.out.println("DEBUG evaluateGetterOnReceiver: fallback receiverType=" + receiverType);
}
if (receiverType != null) {
String simpleReceiverType = receiverType;
if (receiverType.contains("<")) {
simpleReceiverType = receiverType.substring(0, receiverType.indexOf('<'));
}
System.out.println("DEBUG evaluateGetterOnReceiver: simpleReceiverType=" + simpleReceiverType);
TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType);
if (td != null) {
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
if (getter != null && getter.getBody() != null) {
Map<String, String> fieldValues = new HashMap<>();
String varName = receiverName;
org.eclipse.jdt.core.dom.Expression initExpr = findVariableInitializer(entryMethod, varName);
System.out.println("DEBUG evaluateGetterOnReceiver: initExpr=" + initExpr + " class=" + (initExpr != null ? initExpr.getClass().getName() : "null"));
if (initExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor));
System.out.println("DEBUG evaluateGetterOnReceiver: fieldValues=" + fieldValues);
int lastDot = entryMethod.lastIndexOf('.');
if (lastDot > 0) {
String className = entryMethod.substring(0, lastDot);
String methodName = entryMethod.substring(lastDot + 1);
TypeDeclaration entryTd = context.getTypeDeclaration(className);
if (entryTd != null) {
MethodDeclaration entryMd = context.findMethodDeclaration(entryTd, methodName, false);
if (entryMd != null && entryMd.getBody() != null) {
// Skip trackSetterCallsBetween since getter call is not in entry method
}
}
}
}
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
System.out.println("DEBUG evaluateGetterOnReceiver: result=" + result);
if (result != null) {
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
if (returnType != null && !result.contains(".")) {
int lastDot = returnType.lastIndexOf('.');
String simpleReturnType = lastDot > 0 ? returnType.substring(lastDot + 1) : returnType;
result = simpleReturnType + "." + result;
}
return Collections.singletonList(result);
}
}
}
}
}
return null;
}
private org.eclipse.jdt.core.dom.Expression findVariableInitializer(String methodFqn, String varName) {
int lastDot = methodFqn.lastIndexOf('.');
if (lastDot < 0) return null;
String className = methodFqn.substring(0, lastDot);
String methodName = methodFqn.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) return null;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
if (md == null || md.getBody() == null) return null;
final org.eclipse.jdt.core.dom.Expression[] initExpr = {null};
md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName) && frag.getInitializer() != null) {
initExpr[0] = frag.getInitializer();
return false;
}
}
return super.visit(node);
}
});
return initExpr[0];
}
}

View File

@@ -49,7 +49,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
}
}
} else {
@@ -62,7 +68,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
}
if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) {
@@ -110,62 +122,25 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
return graph;
}
private List<String> resolveArguments(List<?> astArguments) {
List<String> args = new ArrayList<>();
for (Object argObj : astArguments) {
Expression expr = (Expression) argObj;
// Extract from lambda
if (expr instanceof LambdaExpression le) {
ASTNode body = le.getBody();
if (body instanceof Expression bodyExpr) {
expr = bodyExpr;
} else if (body instanceof Block block) {
for (Object stmtObj : block.statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
if (expr instanceof ExpressionMethodReference emr) {
TypeDeclaration td = findEnclosingType(expr);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
if (md != null && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
}
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
Expression tracedExpr = traceVariable(expr);
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
}
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
Expression firstArg = (Expression) cic.arguments().get(0);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg; // Only unwrap if it's actually a constant
}
}
String val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
}
args.add(val);
@Override
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
org.eclipse.jdt.core.dom.Expression unwrappedBuilder = unwrapMessageBuilder(expr);
if (unwrappedBuilder != expr) {
expr = unwrappedBuilder;
}
return args;
// Trace variable to resolve local variable references
org.eclipse.jdt.core.dom.Expression tracedExpr = traceVariable(expr);
if (tracedExpr instanceof org.eclipse.jdt.core.dom.QualifiedName
|| tracedExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation
|| tracedExpr instanceof org.eclipse.jdt.core.dom.StringLiteral
|| tracedExpr instanceof org.eclipse.jdt.core.dom.NumberLiteral) {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
}
// Delegate to base class for lambda, method reference, and constructor unwrapping
return super.resolveArgument(expr);
}
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
@@ -357,4 +332,22 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
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;
}
}

View File

@@ -36,4 +36,62 @@ public final class AstUtils {
return type.toString(); // fallback
}
}
public static boolean isFunctionalCollectionMethod(org.eclipse.jdt.core.dom.MethodInvocation node) {
if (node.getExpression() == null) {
return false;
}
org.eclipse.jdt.core.dom.ITypeBinding typeBinding = node.getExpression().resolveTypeBinding();
if (typeBinding != null) {
if (isCollectionOrStreamType(typeBinding)) {
return true;
}
return false;
}
String methodName = node.getName().getIdentifier();
if (methodName.equals("forEach") || methodName.equals("map") ||
methodName.equals("filter") || methodName.equals("flatMap") ||
methodName.equals("anyMatch") || methodName.equals("allMatch") ||
methodName.equals("noneMatch") || methodName.equals("reduce") ||
methodName.equals("collect") || methodName.equals("removeIf") ||
methodName.equals("ifPresent") || methodName.equals("ifPresentOrElse") ||
methodName.equals("compute") || methodName.equals("computeIfAbsent") ||
methodName.equals("computeIfPresent") || methodName.equals("replaceAll") ||
methodName.equals("merge") || methodName.equals("peek") ||
methodName.equals("doOnNext") || methodName.equals("doOnSuccess") ||
methodName.equals("doOnError") || methodName.equals("concatMap") ||
methodName.equals("switchMap")) {
return true;
}
return false;
}
private static boolean isCollectionOrStreamType(org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
String fqn = typeBinding.getErasure().getQualifiedName();
if (fqn.startsWith("java.util.stream.") ||
fqn.equals("java.lang.Iterable") ||
fqn.equals("java.util.Collection") ||
fqn.equals("java.util.List") ||
fqn.equals("java.util.Set") ||
fqn.equals("java.util.Map") ||
fqn.equals("java.util.Iterator") ||
fqn.equals("java.util.Optional") ||
fqn.equals("reactor.core.publisher.Flux") ||
fqn.equals("reactor.core.publisher.Mono")) {
return true;
}
for (org.eclipse.jdt.core.dom.ITypeBinding intf : typeBinding.getInterfaces()) {
if (isCollectionOrStreamType(intf)) return true;
}
if (typeBinding.getSuperclass() != null) {
return isCollectionOrStreamType(typeBinding.getSuperclass());
}
return false;
}
}

View File

@@ -65,7 +65,7 @@ public class CodebaseContext {
return Collections.unmodifiableList(libraryHints);
}
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/**", "**/node_modules/**", "**/out/**"));
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/classes/**", "**/build/libs/**", "**/build/tmp/**", "**/build/reports/**", "**/build/test-results/**", "**/node_modules/**", "**/out/**"));
private String[] classpath = new String[0];
private String[] sourcepath = new String[0];
private boolean resolveBindings = false;
@@ -191,6 +191,8 @@ public class CodebaseContext {
indexType(td, packageName, javaFile);
} else if (type instanceof EnumDeclaration ed) {
indexEnum(ed, packageName, javaFile);
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration rd) {
indexRecord(rd, packageName, javaFile);
}
}
}
@@ -235,6 +237,33 @@ public class CodebaseContext {
}
}
private void indexRecord(org.eclipse.jdt.core.dom.RecordDeclaration rd, String parentFqn, Path javaFile) {
String simpleName = rd.getName().getIdentifier();
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
// Treat as a class since it is a type
classes.put(fqn, (CompilationUnit) rd.getRoot());
classPaths.put(fqn, javaFile);
if (simpleNameToFqn.containsKey(simpleName)) {
String existingFqn = simpleNameToFqn.get(simpleName);
if (!existingFqn.equals(fqn)) {
ambiguousSimpleNames.add(simpleName);
}
} else {
simpleNameToFqn.put(simpleName, fqn);
}
// Recursively index nested types
for (Object type : rd.bodyDeclarations()) {
if (type instanceof TypeDeclaration nestedTd) {
indexType(nestedTd, fqn, javaFile);
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
indexRecord(nestedRd, fqn, javaFile);
}
}
}
public List<String> getImplementations(String interfaceName) {
Set<String> allImpls = new HashSet<>();
collectImplementations(interfaceName, allImpls, new HashSet<>());

View File

@@ -45,6 +45,7 @@ public class ExportService {
new EntryPointEnricher(),
new PropertyEnricher(),
new CallChainEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
)));
}

View File

@@ -152,4 +152,334 @@ public class ConstantResolverTest {
String result = resolver.resolve(returnExpr, context);
assertThat(result).isEqualTo("${app.path}");
}
@Test
void testConstructorAssignmentAndFieldAccess(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("EventDto.java"),
"package com.example;\n" +
"public class EventDto {\n" +
" private String eventType;\n" +
" public EventDto() {\n" +
" this.eventType = \"PAYMENT_SUCCESS\";\n" +
" }\n" +
" public String getEventType() {\n" +
" return this.eventType;\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.example.EventDto");
MethodDeclaration method = td.getMethods()[1]; // getEventType
ReturnStatement rs = (ReturnStatement) method.getBody().statements().get(0);
Expression returnExpr = rs.getExpression(); // this.eventType
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(returnExpr, context);
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");
}
}

View File

@@ -0,0 +1,61 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class AnonymousClassTests {
@TempDir
Path tempDir;
@Test
void shouldResolveEventFromAnonymousClass() throws Exception {
String source = """
package com.example;
public class AnonymousService {
private EventService service;
public void process() {
MyEvent event = new MyEvent() {
@Override
public String getEvent() {
return "ANON_EVENT";
}
};
service.trigger(event.getEvent());
}
}
class EventService {
public void trigger(String ev) {}
}
class MyEvent {
public String getEvent() { return "BASE_EVENT"; }
}
""";
Files.writeString(tempDir.resolve("AnonymousService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("AnonymousService.java")
.sourceModule("com.example")
.event("event.getEvent()")
.lineNumber(12)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.AnonymousService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
assertThat(result.getPolymorphicEvents()).contains("ANON_EVENT");
}
}

View File

@@ -0,0 +1,54 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class CollectionIndexingTests {
@TempDir
Path tempDir;
@Test
void shouldResolveEventFromArrayAccess() throws Exception {
String source = """
package com.example;
public class ArrayService {
private EventService service;
public void process() {
String[] events = new String[] {"EVENT_1", "EVENT_2"};
service.trigger(events[0]);
}
}
class EventService {
public void trigger(String ev) {}
}
""";
Files.writeString(tempDir.resolve("ArrayService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("ArrayService.java")
.sourceModule("com.example")
.event("events[0]")
.lineNumber(7)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.ArrayService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
// When using array indexing, we should ideally extract the indexed element or at least all elements in the array
assertThat(result.getPolymorphicEvents()).contains("EVENT_1");
}
}

View File

@@ -89,7 +89,8 @@ public class GenericEventDetectorTest {
"import org.springframework.statemachine.StateMachine;\n" +
"public class MyService {\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" +
" stateMachine.sendEvent(this.getEvent());\n" +
" }\n" +

View File

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

View File

@@ -0,0 +1,66 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class StreamApiTests {
@TempDir
Path tempDir;
@Test
void shouldHandleStreamApiChains() throws Exception {
String source = """
package com.example;
import java.util.List;
public class StreamService {
private EventService service;
public void process(List<MyEvent> events) {
events.stream()
.filter(e -> e.isValid())
.map(e -> e.toEvent())
.forEach(t -> service.trigger(t.getEvent()));
}
}
class EventService {
public void trigger(String ev) {}
}
class MyEvent {
public boolean isValid() { return true; }
public OrderEvent toEvent() { return new OrderEvent(); }
}
class OrderEvent {
public String getEvent() { return "STREAM_EVENT"; }
}
""";
Files.writeString(tempDir.resolve("StreamService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("StreamService.java")
.sourceModule("com.example")
.event("t.getEvent()")
.lineNumber(11)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.StreamService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
System.out.println("TEST RESULT: " + result);
System.out.println("POLY EVENTS: " + result.getPolymorphicEvents());
assertThat(result.getPolymorphicEvents()).isNotNull().containsExactlyInAnyOrder("STREAM_EVENT");
}
}

View File

@@ -192,7 +192,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PLACE_ORDER" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -227,7 +227,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21,
"polymorphicEvents" : null
"polymorphicEvents" : [ "CANCEL_ORDER" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -288,7 +288,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PAY_ORDER" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -50,16 +50,6 @@
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
}, {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
}, {
"event" : "SUBMIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -355,37 +345,6 @@
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/submit",
"className" : "click.kamil.examples.statemachine.extended.web.OrderController",
"methodName" : "submitOrder",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/submit",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
"triggerPoint" : {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "START",
"targetState" : "PROCESSING",
"event" : "EXTERNAL_TRIGGER"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/submit",
@@ -408,7 +367,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20,
"polymorphicEvents" : null
"polymorphicEvents" : [ "SUBMIT_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -439,7 +398,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : [ "CANCEL_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -536,7 +495,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : [ "REACTIVE_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -597,7 +556,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -624,7 +583,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -651,7 +610,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -678,7 +637,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -705,7 +664,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -732,7 +691,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -759,7 +718,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -786,7 +745,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -813,7 +772,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -840,7 +799,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -867,7 +826,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -894,7 +853,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -921,7 +880,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -948,7 +907,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -975,7 +934,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1002,7 +961,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1029,7 +988,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1056,7 +1015,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1083,7 +1042,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1110,7 +1069,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1137,7 +1096,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1164,7 +1123,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1191,7 +1150,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1218,7 +1177,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1245,7 +1204,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1272,7 +1231,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1299,7 +1258,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1326,7 +1285,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1353,7 +1312,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1380,7 +1339,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1407,7 +1366,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1434,7 +1393,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1461,7 +1420,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1488,7 +1447,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1515,7 +1474,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null

View File

@@ -50,16 +50,6 @@
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
}, {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
}, {
"event" : "SUBMIT_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
@@ -355,37 +345,6 @@
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/submit",
"className" : "click.kamil.examples.statemachine.extended.web.OrderController",
"methodName" : "submitOrder",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/submit",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
"triggerPoint" : {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "START",
"targetState" : "PROCESSING",
"event" : "EXTERNAL_TRIGGER"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/submit",
@@ -408,7 +367,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20,
"polymorphicEvents" : null
"polymorphicEvents" : [ "SUBMIT_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -439,7 +398,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : [ "CANCEL_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -536,7 +495,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : [ "REACTIVE_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -597,7 +556,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -624,7 +583,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -651,7 +610,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -678,7 +637,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -705,7 +664,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -732,7 +691,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -759,7 +718,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -786,7 +745,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -813,7 +772,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -840,7 +799,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -867,7 +826,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -894,7 +853,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -921,7 +880,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -948,7 +907,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -975,7 +934,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1002,7 +961,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1029,7 +988,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1056,7 +1015,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1083,7 +1042,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1110,7 +1069,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1137,7 +1096,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1164,7 +1123,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1191,7 +1150,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1218,7 +1177,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1245,7 +1204,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1272,7 +1231,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1299,7 +1258,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1326,7 +1285,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1353,7 +1312,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1380,7 +1339,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1407,7 +1366,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "FALLBACK_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1434,7 +1393,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PROFILED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1461,7 +1420,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1488,7 +1447,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
@@ -1515,7 +1474,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null

View File

@@ -57,7 +57,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 15,
"polymorphicEvents" : null
"polymorphicEvents" : [ "INHERITED_SUBMIT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -381,10 +381,18 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvents.PAY", "OrderEvents.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
}, {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.CANCEL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
@@ -400,7 +408,7 @@
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "event",
"event" : "new PayEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
@@ -408,25 +416,13 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : null
"polymorphicEvents" : [ "OrderEvents.PAY" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
}, {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.FULFILLED",
"event" : "OrderEvents.FULFILL"
}, {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.CANCEL"
}, {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.ABCD"
} ]
}, {
"entryPoint" : {

View File

@@ -144,7 +144,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -175,7 +175,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -206,7 +206,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -241,7 +241,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -276,7 +276,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 78,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -311,7 +311,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -346,7 +346,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 50,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -1,6 +0,0 @@
[
{
"methodFqn": "ExternalTaxService.calculateTax",
"event": "FINALIZE"
}
]

View File

@@ -1,6 +0,0 @@
[
{
"methodFqn": "ExternalNotificationService.sendExternalNotification",
"event": "EXTERNAL_TRIGGER"
}
]