stage 2 update loop

This commit is contained in:
2026-06-27 19:33:44 +02:00
parent 642f6f8926
commit 8d66af6d24
5 changed files with 365 additions and 14 deletions

View File

@@ -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<Expression> 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<Expression> 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();
}
}

View File

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