forward analysis nemotron3 ultra

forward analysis
This commit is contained in:
2026-06-23 06:20:07 +02:00
parent e31056a44e
commit d946a898cf
14 changed files with 988 additions and 220 deletions

View File

@@ -27,6 +27,7 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
hasPolyMatch = true;
break;
}
continue;
}
String simplePe = pe;
@@ -49,6 +50,7 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent)) {
return true;
}
return false;
}
String triggerEvent = simplify(rawTriggerEvent);

View File

@@ -108,7 +108,32 @@ 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) {
@@ -134,21 +159,52 @@ public class ConstantResolver {
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);
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);
}
/**
* 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;
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);
}
/**
* 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];
md.getBody().accept(new ASTVisitor() {
body.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
@@ -158,9 +214,7 @@ public class ConstantResolver {
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);
}
if (rhsVal != null) localVars.put(varName, rhsVal);
}
}
}
@@ -176,22 +230,16 @@ public class ConstantResolver {
} 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); // alias without 'this.' only if not shadowed
}
if (!declaredLocals.contains(bareName)) localVars.put(bareName, rhsVal);
} else {
localVars.put(varName, rhsVal);
if (!declaredLocals.contains(varName)) {
localVars.put("this." + varName, rhsVal); // alias with 'this.' only if not shadowed
}
if (!declaredLocals.contains(varName)) localVars.put("this." + varName, rhsVal);
}
}
return super.visit(assignment);
@@ -203,7 +251,7 @@ public class ConstantResolver {
String result = evaluateSwitchStatement(ss, localVars, context, visited);
if (result != null) finalResult[0] = result;
}
return super.visit(ss);
return false; // children handled inside evaluateSwitchStatement
}
@Override
@@ -238,6 +286,10 @@ public class ConstantResolver {
} 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;
@@ -270,6 +322,10 @@ public class ConstantResolver {
} 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;
@@ -452,4 +508,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

@@ -236,7 +236,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
} else {
// Fallback for complex chained expressions
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "";
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "this";
if (!exprStr.contains("(")) {
varName = exprStr;
}
@@ -256,7 +256,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
} else if (exprNode instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
varName = sn.getIdentifier();
methodName = "VariableReference"; // We just want to trigger deep trace
// Check if it's a constant (Java UPPER_SNAKE_CASE convention)
// If so, add it directly to polymorphicEvents instead of tracing as a variable
if (varName.equals(varName.toUpperCase()) && varName.contains("_")) {
polymorphicEvents.add(varName);
methodName = null; // Skip variable tracing
} else {
methodName = "VariableReference"; // We just want to trigger deep trace
}
} else if (exprNode instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe &&
pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce &&
ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
@@ -277,10 +284,13 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
List<org.eclipse.jdt.core.dom.Expression> tracedSetters = traceVariableAll(localSetterExpr);
for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) {
extractConstantsFromExpression(tracedSetter, polymorphicEvents);
if (tracedSetter instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic && cic.getAnonymousClassDeclaration() != null) {
resolvedValue = cic.toString() + "." + methodName + "()";
}
}
if (!polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
.event(tp.getEvent())
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
@@ -295,30 +305,91 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
if (varName != null && declaredType == null) {
for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
System.out.println("DEBUG getVariableDeclaredType(" + methodFqn + ", " + varName + ") = " + declaredType);
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
List<String> values = resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu);
System.out.println("DEBUG resolveMethodReturnConstant returned: " + values);
if (values != null && !values.isEmpty()) {
polymorphicEvents.addAll(values);
if (!varName.isEmpty() && !"this".equals(varName) && !"super".equals(varName)) {
TypeDeclaration currentTd = context.getTypeDeclaration(tp.getClassName());
if (currentTd != null) {
MethodDeclaration currentMd = context.findMethodDeclaration(currentTd, tp.getMethodName(), true);
if (currentMd != null) {
final String[] extractedFinalTraced = {null, null};
final String targetVar = varName;
final String mName = methodName;
final String[] finalDeclaredType = {null};
final String[] finalSourceMethod = {null};
currentMd.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
public boolean visit(org.eclipse.jdt.core.dom.VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(targetVar)) {
if (vdf.getInitializer() != null) {
org.eclipse.jdt.core.dom.Expression valueNode = traceVariable(vdf.getInitializer());
if (valueNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
if (cic.getAnonymousClassDeclaration() != null) {
extractedFinalTraced[0] = cic.toString();
extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
} else {
finalDeclaredType[0] = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
finalSourceMethod[0] = "inline-instantiation";
}
} else {
extractedFinalTraced[0] = valueNode.toString();
extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
}
}
}
}
return super.visit(node);
}
});
if (finalDeclaredType[0] != null) {
declaredType = finalDeclaredType[0];
sourceMethod = finalSourceMethod[0];
}
if (extractedFinalTraced[0] != null) {
resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1];
System.out.println("DEBUG localized tracing resolvedValue to: " + resolvedValue);
// Since we updated resolvedValue, we need to extract from it if it's an inline anonymous class
if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) {
org.eclipse.jdt.core.dom.ASTParser p = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17);
p.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
p.setSource(resolvedValue.toCharArray());
org.eclipse.jdt.core.dom.ASTNode newNode = p.createAST(null);
if (newNode instanceof org.eclipse.jdt.core.dom.Expression) {
List<org.eclipse.jdt.core.dom.Expression> traced = traceVariableAll((org.eclipse.jdt.core.dom.Expression) newNode);
for (org.eclipse.jdt.core.dom.Expression ex : traced) {
extractConstantsFromExpression(ex, polymorphicEvents);
}
}
}
}
}
sourceMethod = methodFqn;
break;
}
}
// If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent())
if (declaredType == null && varName.matches("^[A-Z].*")) {
org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName);
if (staticTd != null) {
declaredType = context.getFqn(staticTd);
sourceMethod = "static-call";
} else if (context.getEnumValues(varName) != null) {
declaredType = varName;
sourceMethod = "static-call";
if (declaredType == null) {
for (String methodFqn : path) {
declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
System.out.println("DEBUG getVariableDeclaredType(" + methodFqn + ", " + varName + ") = " + declaredType);
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
List<String> values = resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu);
System.out.println("DEBUG resolveMethodReturnConstant returned: " + values);
if (values != null && !values.isEmpty()) {
polymorphicEvents.addAll(values);
}
sourceMethod = methodFqn;
break;
}
}
// If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent())
if (declaredType == null && varName.matches("^[A-Z].*")) {
org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName);
if (staticTd != null) {
declaredType = context.getFqn(staticTd);
sourceMethod = "static-call";
} else if (context.getEnumValues(varName) != null) {
declaredType = varName;
sourceMethod = "static-call";
}
}
}
}
@@ -868,7 +939,19 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
}
}
if (!handledByLombok && methodName != null && !methodName.isEmpty()) {
// Try proper getter evaluation: builds field→value from constructor args,
// then evaluates the getter body (handles switch-based enum transformations).
java.util.List<String> narrowed = resolveInlineInstantiationGetterResult(
cic, methodName, context, new java.util.HashSet<>());
if (!narrowed.isEmpty()) {
constants.addAll(narrowed);
handledByLombok = true; // reuse flag to skip fallback below
}
}
if (!handledByLombok) {
// Fallback: extract all constructor args (catches cases where getter
// evaluation is not possible, e.g. no source for the wrapper class)
for (Object argObj : cic.arguments()) {
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants);
}
@@ -897,6 +980,146 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
}
/**
* Builds a map of {@code fieldName → resolved-value} (and {@code this.fieldName → value})
* by correlating the actual arguments of the given {@link ClassInstanceCreation} with the
* field assignments found in the matching constructor body.
*
* <p>Handles both {@code this.field = param} and bare {@code field = param} assignment forms.
*/
protected java.util.Map<String, String> buildFieldValuesFromCIC(
org.eclipse.jdt.core.dom.TypeDeclaration td,
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
CodebaseContext context) {
java.util.Map<String, String> fieldValues = new java.util.HashMap<>();
for (org.eclipse.jdt.core.dom.MethodDeclaration ctor : td.getMethods()) {
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
if (ctor.parameters().size() != cic.arguments().size()) continue;
// 1. Map each parameter name → resolved argument value
java.util.Map<String, String> paramValues = new java.util.HashMap<>();
for (int i = 0; i < ctor.parameters().size(); i++) {
String pName = ((org.eclipse.jdt.core.dom.SingleVariableDeclaration)
ctor.parameters().get(i)).getName().getIdentifier();
org.eclipse.jdt.core.dom.Expression argExpr =
(org.eclipse.jdt.core.dom.Expression) cic.arguments().get(i);
java.util.List<String> consts = new java.util.ArrayList<>();
extractConstantsFromArgument(argExpr, consts);
if (!consts.isEmpty()) {
paramValues.put(pName, consts.get(0));
} else {
String resolved = constantResolver.resolve(argExpr, context);
if (resolved != null) paramValues.put(pName, resolved);
}
}
if (paramValues.isEmpty()) continue;
// 2. Scan constructor body for field assignments: this.field = param OR field = param
ctor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.Assignment node) {
org.eclipse.jdt.core.dom.Expression lhs = node.getLeftHandSide();
org.eclipse.jdt.core.dom.Expression rhs = node.getRightHandSide();
String fieldName = null;
if (lhs instanceof org.eclipse.jdt.core.dom.FieldAccess fa
&& fa.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression) {
fieldName = fa.getName().getIdentifier();
} else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn
&& !paramValues.containsKey(sn.getIdentifier())) {
// Bare field assignment (not a parameter-to-itself assignment)
fieldName = sn.getIdentifier();
}
if (fieldName != null && rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
String paramVal = paramValues.get(snRhs.getIdentifier());
if (paramVal != null) {
fieldValues.put(fieldName, paramVal);
fieldValues.put("this." + fieldName, paramVal);
}
}
return super.visit(node);
}
});
break; // first matching constructor is enough
}
return fieldValues;
}
/**
* Searches for a {@code return new SomeType(...)} statement in the given method, recursing
* into interface implementations when the method body is absent.
*/
protected org.eclipse.jdt.core.dom.ClassInstanceCreation findReturnedCIC(
String className, String methodName, CodebaseContext context) {
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
org.eclipse.jdt.core.dom.MethodDeclaration md =
context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
org.eclipse.jdt.core.dom.ClassInstanceCreation[] result = {null};
md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
result[0] = cic;
}
return false;
}
});
if (result[0] != null) return result[0];
}
}
// Recurse into implementations (handles interfaces / abstract classes)
java.util.List<String> impls = context.getImplementations(className);
for (String impl : impls) {
org.eclipse.jdt.core.dom.ClassInstanceCreation cic = findReturnedCIC(impl, methodName, context);
if (cic != null) return cic;
}
return null;
}
/**
* Resolves the value produced by calling {@code getterName} on an object created via the
* given {@link ClassInstanceCreation}.
*
* <p>Algorithm:
* <ol>
* <li>Build a {@code fieldName → value} map from the CIC's constructor arguments.</li>
* <li>Ask {@link click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver}
* to evaluate the getter body with those values as pre-seeded locals.</li>
* </ol>
*
* <p>This correctly handles both simple pass-through getters ({@code return field}) and
* transformation getters that map one enum to another via a switch expression.
*
* @return resolved values, or an empty list when resolution is not possible
*/
private java.util.List<String> resolveInlineInstantiationGetterResult(
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
String getterName,
CodebaseContext context,
java.util.Set<String> visited) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils
.extractSimpleTypeName(cic.getType());
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(typeName);
if (td == null) return java.util.Collections.emptyList();
org.eclipse.jdt.core.dom.MethodDeclaration getter =
context.findMethodDeclaration(td, getterName, true);
if (getter == null || getter.getBody() == null) return java.util.Collections.emptyList();
java.util.Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context);
if (fieldValues.isEmpty()) return java.util.Collections.emptyList();
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
return result != null ? java.util.List.of(result) : java.util.Collections.emptyList();
}
private int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) {
boolean isAllArgsConstructor = false;
boolean isRequiredArgsConstructor = false;

View File

@@ -162,8 +162,19 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
expr = firstArg; // Only unwrap if it's actually a constant
}
}
String val = constantResolver.resolve(expr, context);
String val = null;
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized
// from a ClassInstanceCreation (directly or via a factory method). This avoids
// the broad ENUM_SET fallback when the getter transforms one enum to another.
if (expr instanceof MethodInvocation getterMi
&& getterMi.getExpression() instanceof SimpleName instanceSn
&& getterMi.arguments().isEmpty()) {
val = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
}
if (val == null) val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
}
@@ -317,4 +328,163 @@ 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);
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);
}
}
}
}
}
}
}
}
}

View File

@@ -158,6 +158,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
}
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
Expression unwrappedBuilder = unwrapMessageBuilder(expr);
if (unwrappedBuilder != expr) {
expr = unwrappedBuilder;
}
Expression tracedExpr = traceVariable(expr);
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
@@ -369,4 +374,22 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
return null;
}
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

@@ -378,4 +378,108 @@ public class ConstantResolverTest {
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

@@ -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

@@ -1002,9 +1002,10 @@ class HeuristicCallGraphEngineTest {
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
// It successfully traces the local variable 'event' to its anonymous class initializer
assertThat(chain.getTriggerPoint().getEvent()).startsWith("new RichEvent(){");
assertThat(chain.getTriggerPoint().getEvent()).endsWith(".getType()");
// The engine traces the anonymous class's getType() through the RichEvent interface,
// resolving to OrderEvents.CREATE via the enum return-type fallback.
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.CREATE");
}
@Test
@@ -2753,4 +2754,241 @@ class HeuristicCallGraphEngineTest {
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("MyEvents.A", "MyEvents.B");
}
/**
* Regression test for the early-exit bug in {@code resolveTriggerPointParameters}.
*
* <p>When {@code traceLocalSetter} matches a field method call whose name happens to equal the
* accessor suffix (e.g. {@code mapper.transform(dto)} matching {@code methodName="transform"}),
* and the argument passed to that method is a non-constant runtime parameter, no events are
* extracted. The old condition
* <pre>if (!polymorphicEvents.isEmpty() || !resolvedValue.equals(tp.getEvent()))</pre>
* caused an early return with empty {@code polymorphicEvents} because {@code resolvedValue} had
* already diverged from the original event name.
*
* <p>After the fix the condition is just {@code !polymorphicEvents.isEmpty()}, so resolution
* continues through {@code getVariableDeclaredType} → {@code resolveMethodReturnConstant},
* which correctly traces the field's declared type to its implementation and extracts the
* constant returned by {@code TransformerImpl.transform()}.
*/
@Test
void shouldResolveEventsWhenFieldTransformerMethodMatchesSetterPatternButArgIsNonConstant() throws IOException {
String source = """
package com.example;
public class OrderController {
private Transformer transformer;
public void processEvent(String dto) {
Event e = transformer.transform(dto);
machine.fire(e.getType());
}
}
interface Transformer {
Event transform(String input);
}
class TransformerImpl implements Transformer {
public Event transform(String input) {
return new Event("ORDER_DISPATCHED");
}
}
class Event {
private String type;
public Event(String type) { this.type = type; }
public String getType() { return type; }
}
class Machine {
public void fire(String event) {}
}
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_transformer");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.Machine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
// Must resolve through TransformerImpl.transform() → "ORDER_DISPATCHED",
// NOT return empty polymorphicEvents due to early exit on the setter match.
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("ORDER_DISPATCHED");
}
/**
* Diagnostic: enum passed to a wrapper object constructor, then retrieved via getter.
*
* Pattern:
* entry: event = mapper.toEvent(dto); machine.fire(event.getType());
* mapper: OrderMapperImpl.toEvent → new OrderEvent(States.PAYMENT_RECEIVED)
* wrapper: OrderEvent(States type) { this.type = type; } getType() { return type; }
*
* Expected: polymorphicEvents = ["States.PAYMENT_RECEIVED"]
* Tests whether the engine traces: getter → field → constructor arg → constant.
*/
@Test
void shouldResolveEnumPassedThroughWrapperObjectAndGetter() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderMapper mapper;
private StateMachine machine;
public void processOrder(String dto) {
OrderEvent event = mapper.toEvent(dto);
machine.fire(event.getType());
}
}
interface OrderMapper {
OrderEvent toEvent(String input);
}
class OrderMapperImpl implements OrderMapper {
public OrderEvent toEvent(String input) {
return new OrderEvent(States.PAYMENT_RECEIVED);
}
}
class OrderEvent {
private States type;
public OrderEvent(States type) { this.type = type; }
public States getType() { return type; } // bare field, no this.
}
class StateMachine {
public void fire(States event) {}
}
enum States { PAYMENT_RECEIVED, CANCELLED }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_wrapper");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrder")
.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("DIAGNOSTIC polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("States.PAYMENT_RECEIVED");
}
/**
* Two-enum transformation: a DTO-level enum (OrderType) is passed into a wrapper
* constructor. The wrapper's getter transforms it via a switch expression to a
* state-machine enum (SMEvent). The engine must return the SM enum, NOT the DTO enum.
*
* Pattern:
* entry: msg = mapper.toMsg(dto); machine.fire(msg.getSmEvent());
* wrapper: OrderMsg(OrderType t) { this.type = t; }
* getSmEvent() { return switch(type) { case PAY -> SMEvent.PAY_RECEIVED; ... }; }
*
* Root cause of bug: engine extracted the constructor arg (OrderType.PAY) instead of
* evaluating the getter's switch body, returning the wrong enum.
*/
@Test
void shouldResolveTransformingGetterThatMapsDtoEnumToStateMachineEnum() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderMapper mapper;
private StateMachine machine;
public void processOrder(String dto) {
OrderMsg msg = mapper.toMsg(dto);
machine.fire(msg.getSmEvent());
}
}
interface OrderMapper {
OrderMsg toMsg(String input);
}
class OrderMapperImpl implements OrderMapper {
public OrderMsg toMsg(String input) {
return new OrderMsg(OrderType.PAY);
}
}
class OrderMsg {
private OrderType type;
public OrderMsg(OrderType type) { this.type = type; }
public SMEvent getSmEvent() {
return switch (type) {
case PAY -> SMEvent.PAY_RECEIVED;
case CANCEL -> SMEvent.ORDER_CANCELLED;
};
}
}
class StateMachine {
public void fire(SMEvent event) {}
}
enum OrderType { PAY, CANCEL }
enum SMEvent { PAY_RECEIVED, ORDER_CANCELLED }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_twoenum");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrder")
.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("TWO-ENUM polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// Must return the SM enum, NOT the DTO enum (OrderType.PAY would be wrong)
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED");
}
}

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" : [ {