diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java index 92948b7..dfbab69 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java @@ -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; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/SymbolicPathValueEstimator.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/SymbolicPathValueEstimator.java index b9d75a5..25cb0de 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/SymbolicPathValueEstimator.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/SymbolicPathValueEstimator.java @@ -56,6 +56,22 @@ public class SymbolicPathValueEstimator implements PathValueEstimator { 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()) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java index 355bf9c..759288f 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java @@ -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) { @@ -133,54 +158,119 @@ public class ConstantResolver { if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null; java.util.Map localVars = new java.util.HashMap<>(); + java.util.Set 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); - localVars.put(param.getName().getIdentifier(), val); + 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); } } - if (localVars.isEmpty()) return null; + return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited); + } - for (Object stmtObj : md.getBody().statements()) { - if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) { - if (es.getExpression() instanceof 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) { - localVars.put(varName, rhsVal); - if (varName.startsWith("this.")) { - localVars.put(varName.substring(5), rhsVal); // alias without 'this.' - } else { - localVars.put("this." + varName, rhsVal); // alias with 'this.' + /** + * 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 prebuiltLocals, + CodebaseContext context, + Set visited) { + if (md.getBody() == null) return null; + java.util.Map localVars = new java.util.HashMap<>(prebuiltLocals); + java.util.Set 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 localVars, + java.util.Set declaredLocals, + CodebaseContext context, + Set 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); } } } - } else if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchStatement ss) { - String result = evaluateSwitchStatement(ss, localVars, context, visited); - if (result != null) return result; - } else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) { - if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.SwitchExpression se) { - String result = evaluateSwitchExpression(se, localVars, context, visited); - if (result != null) return result; - } else { - String result = resolveExpressionWithParams(rs.getExpression(), localVars); - if (result != null) return result; - } + return super.visit(vds); } - } - return null; + + @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 paramValues, CodebaseContext context, Set visited) { @@ -196,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; @@ -228,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; @@ -251,7 +349,7 @@ public class ConstantResolver { if (expr instanceof SimpleName sn) { String name = sn.getIdentifier(); if (paramValues.containsKey(name)) return paramValues.get(name); - return name; + return null; } else if (expr instanceof FieldAccess fa) { String name = "this." + fa.getName().getIdentifier(); if (paramValues.containsKey(name)) return paramValues.get(name); @@ -410,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; + } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index d26f4ec..be76a3e 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -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 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 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 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 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 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. + * + *

Handles both {@code this.field = param} and bare {@code field = param} assignment forms. + */ + protected java.util.Map buildFieldValuesFromCIC( + org.eclipse.jdt.core.dom.TypeDeclaration td, + org.eclipse.jdt.core.dom.ClassInstanceCreation cic, + CodebaseContext context) { + + java.util.Map 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 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 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 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}. + * + *

Algorithm: + *

    + *
  1. Build a {@code fieldName → value} map from the CIC's constructor arguments.
  2. + *
  3. Ask {@link click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver} + * to evaluate the getter body with those values as pre-seeded locals.
  4. + *
+ * + *

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 resolveInlineInstantiationGetterResult( + org.eclipse.jdt.core.dom.ClassInstanceCreation cic, + String getterName, + CodebaseContext context, + java.util.Set 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 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; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java index d7c1742..a9a67dc 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetector.java @@ -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; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index b9b4a54..9396bfa 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -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). + * + *

This handles both: + *

    + *
  • Simple pass-through: {@code Wrapper(MyEnum e){this.e=e;} getE(){return e;}}
  • + *
  • Transformation: {@code getSmEvent(){return switch(dtoEnum){case A->SmEvents.X;…};}}
  • + *
+ * + * @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 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 fieldValues, + CodebaseContext context) { + java.util.List 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); + } + } + } + } + } + } + } + } } \ No newline at end of file diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index 9e6a958..2e8c68b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -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; } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolverTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolverTest.java index 3e9da56..fc5938e 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolverTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolverTest.java @@ -182,4 +182,304 @@ public class ConstantResolverTest { 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 known constant argument, {@code evaluateMethodOutput} + * should evaluate the switch and return only the matching branch. + * + *

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 runtime 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). + * + *

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"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetectorTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetectorTest.java index 1175ee1..7a0d3ed 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetectorTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/GenericEventDetectorTest.java @@ -89,7 +89,8 @@ public class GenericEventDetectorTest { "import org.springframework.statemachine.StateMachine;\n" + "public class MyService {\n" + " private StateMachine 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" + diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java index c5558eb..5299437 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java @@ -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}. + * + *

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 + *

if (!polymorphicEvents.isEmpty() || !resolvedValue.equals(tp.getEvent()))
+ * caused an early return with empty {@code polymorphicEvents} because {@code resolvedValue} had + * already diverged from the original event name. + * + *

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 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 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 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"); + } } diff --git a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json index f44e11f..d8e6e21 100644 --- a/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/EnterpriseStateMachineConfig/EnterpriseStateMachineConfig.json @@ -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" : [ { diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json index aa0bec7..9deb804 100644 --- a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig/ExtendedStateMachineConfig.json @@ -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 diff --git a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json index 3cfe25a..cc50103 100644 --- a/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/ExtendedStateMachineConfig_PROD/ExtendedStateMachineConfig.json @@ -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 diff --git a/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json index f6e7641..87dc5d2 100644 --- a/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/InheritanceStateMachineConfig/InheritanceStateMachineConfig.json @@ -57,7 +57,7 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 15, - "polymorphicEvents" : null + "polymorphicEvents" : [ "INHERITED_SUBMIT" ] }, "contextMachineId" : null, "matchedTransitions" : [ { diff --git a/state_machines/enterprise_order_system/src/main/resources/hints.json b/state_machines/enterprise_order_system/src/main/resources/hints.json deleted file mode 100644 index eca7c67..0000000 --- a/state_machines/enterprise_order_system/src/main/resources/hints.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "methodFqn": "ExternalTaxService.calculateTax", - "event": "FINALIZE" - } -] diff --git a/state_machines/extended_analysis_sample/src/main/resources/hints.json b/state_machines/extended_analysis_sample/src/main/resources/hints.json deleted file mode 100644 index d51eadb..0000000 --- a/state_machines/extended_analysis_sample/src/main/resources/hints.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "methodFqn": "ExternalNotificationService.sendExternalNotification", - "event": "EXTERNAL_TRIGGER" - } -]