From 8d66af6d246b5776e264f63869f588169449f5fd Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sat, 27 Jun 2026 19:33:44 +0200 Subject: [PATCH] stage 2 update loop --- .../ast/common/CfgBuilder.java | 7 +- .../ast/common/JdtDataFlowModel.java | 181 +++++++++++++++++- .../ast/common/ReachingDefinitions.java | 9 +- .../ast/common/JdtDataFlowModelTest.java | 162 ++++++++++++++++ .../PolymorphicStateMachineConfiguration.json | 20 +- 5 files changed, 365 insertions(+), 14 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CfgBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CfgBuilder.java index 920a0b8..434cb60 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CfgBuilder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CfgBuilder.java @@ -76,13 +76,16 @@ public class CfgBuilder { } if (node instanceof DoStatement doStmt) { - List bodyExits = buildFlow(doStmt.getBody(), sources, cfg); ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT); cfg.addNode(condNode); + + List bodySources = new ArrayList<>(sources); + bodySources.add(condNode); + + List bodyExits = buildFlow(doStmt.getBody(), bodySources, cfg); for (ControlFlowGraph.CfgNode exit : bodyExits) { exit.addSuccessor(condNode); } - condNode.addSuccessor(condNode); // Loop back return List.of(condNode); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java index 66d1c77..9845c36 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java @@ -33,13 +33,57 @@ public class JdtDataFlowModel implements DataFlowModel { @Override public List getReachingDefinitions(Expression expr) { - return getReachingDefinitions(expr, new HashSet<>()); + return getReachingDefinitions(expr, new HashSet<>(), new HashMap<>(), 0); } - private List getReachingDefinitions(Expression expr, Set visited) { - if (expr == null || !visited.add(expr)) return List.of(); + private List getReachingDefinitions( + Expression expr, + Set visited, + Map paramBindings, + int depth) { + if (expr == null || depth > 50 || !visited.add(expr)) { + return List.of(); + } + // 1. Handle Ternary / Conditional Expression + if (expr instanceof ConditionalExpression ce) { + String condVal = resolveValue(ce.getExpression(), context); + if ("true".equals(condVal)) { + List resolved = getReachingDefinitions(ce.getThenExpression(), visited, paramBindings, depth + 1); + visited.remove(expr); + return resolved; + } else if ("false".equals(condVal)) { + List resolved = getReachingDefinitions(ce.getElseExpression(), visited, paramBindings, depth + 1); + visited.remove(expr); + return resolved; + } else { + visited.remove(expr); + return List.of(ce); + } + } + + // 2. Handle Field constant / field initializer propagation + IVariableBinding vb = getVariableBinding(expr); + if (vb != null && vb.isField()) { + Expression fieldInit = findFieldInitializer(vb, expr); + if (fieldInit != null) { + List resolved = getReachingDefinitions(fieldInit, visited, paramBindings, depth + 1); + visited.remove(expr); + return resolved; + } + } + + // 3. Handle Parameter mapping for SimpleName if (expr instanceof SimpleName sn) { + IBinding b = sn.resolveBinding(); + if (b instanceof IVariableBinding paramVb && paramBindings.containsKey(paramVb)) { + Expression argExpr = paramBindings.get(paramVb); + List resolved = getReachingDefinitions(argExpr, visited, paramBindings, depth + 1); + visited.remove(expr); + return resolved; + } + + // Fallback: Local Reaching Definitions MethodDeclaration md = findEnclosingMethod(sn); ReachingDefinitions rd = getReachingDefinitionsForMethod(md); if (rd != null) { @@ -49,7 +93,7 @@ public class JdtDataFlowModel implements DataFlowModel { for (ASTNode def : defs) { Expression defExpr = ReachingDefinitions.getDefinitionExpression(def); if (defExpr != null) { - exprs.addAll(getReachingDefinitions(defExpr, visited)); + exprs.addAll(getReachingDefinitions(defExpr, visited, paramBindings, depth + 1)); } else { exprs.add(expr); } @@ -59,11 +103,138 @@ public class JdtDataFlowModel implements DataFlowModel { } } } - + + // 4. Handle Method Invocations (Static Factory methods / Transforming Getters) + if (expr instanceof MethodInvocation mi) { + IMethodBinding mb = mi.resolveMethodBinding(); + if (mb != null) { + MethodDeclaration md = findMethodDeclaration(mb); + if (md != null && md.getBody() != null) { + Map newParamBindings = new HashMap<>(paramBindings); + List parameters = md.parameters(); + List arguments = mi.arguments(); + int count = Math.min(parameters.size(), arguments.size()); + for (int i = 0; i < count; i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i); + IVariableBinding paramVb = svd.resolveBinding(); + if (paramVb != null) { + newParamBindings.put(paramVb, (Expression) arguments.get(i)); + } + } + + List returns = findReturnStatements(md.getBody()); + if (!returns.isEmpty()) { + List results = new ArrayList<>(); + for (ReturnStatement rs : returns) { + if (rs.getExpression() != null) { + results.addAll(getReachingDefinitions(rs.getExpression(), visited, newParamBindings, depth + 1)); + } + } + visited.remove(expr); + return results; + } + } + } + } + visited.remove(expr); return List.of(expr); } + private IVariableBinding getVariableBinding(Expression expr) { + if (expr instanceof SimpleName sn) { + IBinding b = sn.resolveBinding(); + if (b instanceof IVariableBinding varBinding) return varBinding; + } else if (expr instanceof QualifiedName qn) { + IBinding b = qn.resolveBinding(); + if (b instanceof IVariableBinding varBinding) return varBinding; + } else if (expr instanceof FieldAccess fa) { + return fa.resolveFieldBinding(); + } else if (expr instanceof SuperFieldAccess sfa) { + return sfa.resolveFieldBinding(); + } + return null; + } + + private Expression findFieldInitializer(IVariableBinding vb, ASTNode contextNode) { + if (vb == null) return null; + ITypeBinding declaringClass = vb.getDeclaringClass(); + if (declaringClass == null) return null; + String classFqn = declaringClass.getErasure().getQualifiedName(); + if (classFqn == null || classFqn.isEmpty()) return null; + + TypeDeclaration td = context.getTypeDeclaration(classFqn); + if (td == null) { + CompilationUnit cu = null; + ASTNode root = contextNode.getRoot(); + if (root instanceof CompilationUnit) { + cu = (CompilationUnit) root; + } + td = context.getTypeDeclaration(classFqn, cu); + } + + if (td != null) { + for (FieldDeclaration fd : td.getFields()) { + for (Object fragObj : fd.fragments()) { + if (fragObj instanceof VariableDeclarationFragment fragment) { + if (fragment.getName().getIdentifier().equals(vb.getName())) { + return fragment.getInitializer(); + } + } + } + } + } + return null; + } + + private MethodDeclaration findMethodDeclaration(IMethodBinding mb) { + if (mb == null) return null; + ITypeBinding declaringClass = mb.getDeclaringClass(); + if (declaringClass == null) return null; + String classFqn = declaringClass.getErasure().getQualifiedName(); + if (classFqn == null || classFqn.isEmpty()) return null; + + TypeDeclaration td = context.getTypeDeclaration(classFqn); + if (td != null) { + for (MethodDeclaration md : td.getMethods()) { + IMethodBinding candidateMb = md.resolveBinding(); + if (candidateMb != null && candidateMb.getKey().equals(mb.getKey())) { + return md; + } + } + for (MethodDeclaration md : td.getMethods()) { + if (md.getName().getIdentifier().equals(mb.getName()) && md.parameters().size() == mb.getParameterTypes().length) { + return md; + } + } + } + return null; + } + + private List findReturnStatements(ASTNode node) { + List returns = new ArrayList<>(); + node.accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement rs) { + returns.add(rs); + return super.visit(rs); + } + @Override + public boolean visit(LambdaExpression le) { + return false; + } + @Override + public boolean visit(AnonymousClassDeclaration acd) { + return false; + } + @Override + public boolean visit(TypeDeclarationStatement tds) { + return false; + } + }); + return returns; + } + @Override public String resolveValue(Expression expr, CodebaseContext context) { if (expr == null) return null; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitions.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitions.java index a375372..7981568 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitions.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/ReachingDefinitions.java @@ -38,7 +38,10 @@ public class ReachingDefinitions { } boolean changed = true; - while (changed) { + int iterations = 0; + int maxIterations = 5000; + while (changed && iterations < maxIterations) { + iterations++; changed = false; for (ControlFlowGraph.CfgNode node : cfgNodes) { Set newIn = new HashSet<>(); @@ -67,6 +70,10 @@ public class ReachingDefinitions { } } } + if (iterations >= maxIterations) { + // Log warning or print to stderr + System.err.println("Warning: ReachingDefinitions analysis reached max iterations (" + maxIterations + ") and terminated early."); + } } public Set getReachingDefinitions(ASTNode usageNode, String varName) { diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModelTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModelTest.java index 25e6450..100951e 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModelTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModelTest.java @@ -88,4 +88,166 @@ class JdtDataFlowModelTest { // Both branches assign "SAME", so it converges to "SAME" assertThat(val).isEqualTo("SAME"); } + + @Test + void testFieldConstantPropagation(@TempDir Path tempDir) throws IOException { + Path comFoo = tempDir.resolve("com/foo"); + Files.createDirectories(comFoo); + Files.writeString(comFoo.resolve("Test.java"), + "package com.foo;\n" + + "public class Test {\n" + + " private static final String MY_CONST = \"CONST_VAL\";\n" + + " public void run() {\n" + + " String a = MY_CONST;\n" + + " }\n" + + "}" + ); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); + assertThat(td).isNotNull(); + + MethodDeclaration md = td.getMethods()[0]; + VariableDeclarationStatement aStmt = (VariableDeclarationStatement) md.getBody().statements().get(0); + VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0); + Expression initializer = aFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + String val = model.resolveValue(initializer, context); + assertThat(val).isEqualTo("CONST_VAL"); + } + + @Test + void testTernaryExpressionResolution(@TempDir Path tempDir) throws IOException { + Path comFoo = tempDir.resolve("com/foo"); + Files.createDirectories(comFoo); + Files.writeString(comFoo.resolve("Test.java"), + "package com.foo;\n" + + "public class Test {\n" + + " public void run(boolean flag) {\n" + + " String a = flag ? \"TRUE_VAL\" : \"FALSE_VAL\";\n" + + " String b = true ? \"TRUE_VAL\" : \"FALSE_VAL\";\n" + + " }\n" + + "}" + ); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); + assertThat(td).isNotNull(); + + MethodDeclaration md = td.getMethods()[0]; + + // 1. Unresolvable condition (flag) + VariableDeclarationStatement aStmt = (VariableDeclarationStatement) md.getBody().statements().get(0); + VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0); + Expression initA = aFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + List defsA = model.getReachingDefinitions(initA); + assertThat(defsA).hasSize(1); + assertThat(defsA.get(0)).isInstanceOf(ConditionalExpression.class); + + // 2. Statically resolvable condition (true) + VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1); + VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0); + Expression initB = bFrag.getInitializer(); + + List defsB = model.getReachingDefinitions(initB); + assertThat(defsB).hasSize(1); + assertThat(defsB.get(0)).isInstanceOf(StringLiteral.class); + assertThat(((StringLiteral) defsB.get(0)).getLiteralValue()).isEqualTo("TRUE_VAL"); + } + + @Test + void testStaticFactoryMethodTracking(@TempDir Path tempDir) throws IOException { + Path comFoo = tempDir.resolve("com/foo"); + Files.createDirectories(comFoo); + Files.writeString(comFoo.resolve("Test.java"), + "package com.foo;\n" + + "public class Test {\n" + + " public static String create(String value) {\n" + + " return value;\n" + + " }\n" + + " public void run() {\n" + + " String a = create(\"FACTORY_VAL\");\n" + + " }\n" + + "}" + ); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); + assertThat(td).isNotNull(); + + MethodDeclaration runMd = null; + for (MethodDeclaration md : td.getMethods()) { + if (md.getName().getIdentifier().equals("run")) { + runMd = md; + } + } + assertThat(runMd).isNotNull(); + + VariableDeclarationStatement aStmt = (VariableDeclarationStatement) runMd.getBody().statements().get(0); + VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0); + Expression initializer = aFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + String val = model.resolveValue(initializer, context); + assertThat(val).isEqualTo("FACTORY_VAL"); + } + + @Test + void testDoWhileLoopFlow(@TempDir Path tempDir) throws IOException { + Path comFoo = tempDir.resolve("com/foo"); + Files.createDirectories(comFoo); + Files.writeString(comFoo.resolve("Test.java"), + "package com.foo;\n" + + "public class Test {\n" + + " public void run(boolean flag) {\n" + + " int a = 1;\n" + + " do {\n" + + " a = 2;\n" + + " } while (flag);\n" + + " int b = a;\n" + + " }\n" + + "}" + ); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); + assertThat(td).isNotNull(); + + MethodDeclaration md = td.getMethods()[0]; + ControlFlowGraph cfg = CfgBuilder.build(md); + + // Find the condition node (flag) + ControlFlowGraph.CfgNode condNode = null; + for (ControlFlowGraph.CfgNode node : cfg.getNodes()) { + if (node.getAstNode() instanceof SimpleName sn && sn.getIdentifier().equals("flag")) { + condNode = node; + break; + } + } + assertThat(condNode).isNotNull(); + + // The condition node should loop back to the statement inside the body (a = 2;) + boolean hasLoopBackToBody = false; + for (ControlFlowGraph.CfgNode succ : condNode.getSuccessors()) { + if (succ.getAstNode() instanceof ExpressionStatement es) { + if (es.getExpression() instanceof Assignment assign) { + if (assign.getLeftHandSide() instanceof SimpleName sn && sn.getIdentifier().equals("a")) { + hasLoopBackToBody = true; + } + } + } + } + assertThat(hasLoopBackToBody).isTrue(); + } } diff --git a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json index 2ba364b..d94b499 100644 --- a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json +++ b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json @@ -439,7 +439,7 @@ }, "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderStatic", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], "triggerPoint" : { - "event" : "EventBuilder.buildEvent()", + "event" : "new FulfillEvent()", "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", "methodName" : "processEvent", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", @@ -447,10 +447,14 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 13, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvents.FULFILL" ] }, "contextMachineId" : null, - "matchedTransitions" : null + "matchedTransitions" : [ { + "sourceState" : "OrderStates.PAID", + "targetState" : "OrderStates.FULFILLED", + "event" : "OrderEvents.FULFILL" + } ] }, { "entryPoint" : { "type" : "REST", @@ -466,7 +470,7 @@ }, "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], "triggerPoint" : { - "event" : "new EventBuilder().buildInstanceEvent()", + "event" : "new CancelEvent()", "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", "methodName" : "processEvent", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", @@ -474,10 +478,14 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 13, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvents.CANCEL" ] }, "contextMachineId" : null, - "matchedTransitions" : null + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.CANCELED", + "event" : "OrderEvents.CANCEL" + } ] }, { "entryPoint" : { "type" : "REST",