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..8ffbcc5 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,6 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { hasPolyMatch = true; break; } - continue; // Stricter matching: do not fallback if both have qualifiers but don't match } String simplePe = pe; @@ -50,7 +49,6 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent)) { return true; } - return false; } String triggerEvent = simplify(rawTriggerEvent); 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..1cfbd02 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 @@ -133,54 +133,96 @@ 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); + String paramName = param.getName().getIdentifier(); + localVars.put(paramName, val); + declaredLocals.add(paramName); } } - if (localVars.isEmpty()) return null; + String[] finalResult = new String[1]; - for (Object stmtObj : md.getBody().statements()) { - 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.' + md.getBody().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); // alias without 'this.' only if not shadowed + } + } else { + localVars.put(varName, rhsVal); + if (!declaredLocals.contains(varName)) { + localVars.put("this." + varName, rhsVal); // alias with 'this.' only if not shadowed + } + } + } + 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 super.visit(ss); + } + + @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) { @@ -251,7 +293,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); 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/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..7045082 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,200 @@ 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"); + } }