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

@@ -76,13 +76,16 @@ public class CfgBuilder {
} }
if (node instanceof DoStatement doStmt) { if (node instanceof DoStatement doStmt) {
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(doStmt.getBody(), sources, cfg);
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT); ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(condNode); cfg.addNode(condNode);
List<ControlFlowGraph.CfgNode> bodySources = new ArrayList<>(sources);
bodySources.add(condNode);
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(doStmt.getBody(), bodySources, cfg);
for (ControlFlowGraph.CfgNode exit : bodyExits) { for (ControlFlowGraph.CfgNode exit : bodyExits) {
exit.addSuccessor(condNode); exit.addSuccessor(condNode);
} }
condNode.addSuccessor(condNode); // Loop back
return List.of(condNode); return List.of(condNode);
} }

View File

@@ -33,13 +33,57 @@ public class JdtDataFlowModel implements DataFlowModel {
@Override @Override
public List<Expression> getReachingDefinitions(Expression expr) { public List<Expression> getReachingDefinitions(Expression expr) {
return getReachingDefinitions(expr, new HashSet<>()); return getReachingDefinitions(expr, new HashSet<>(), new HashMap<>(), 0);
} }
private List<Expression> getReachingDefinitions(Expression expr, Set<ASTNode> visited) { private List<Expression> getReachingDefinitions(
if (expr == null || !visited.add(expr)) return List.of(); Expression expr,
Set<ASTNode> visited,
Map<IVariableBinding, Expression> 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<Expression> resolved = getReachingDefinitions(ce.getThenExpression(), visited, paramBindings, depth + 1);
visited.remove(expr);
return resolved;
} else if ("false".equals(condVal)) {
List<Expression> 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<Expression> resolved = getReachingDefinitions(fieldInit, visited, paramBindings, depth + 1);
visited.remove(expr);
return resolved;
}
}
// 3. Handle Parameter mapping for SimpleName
if (expr instanceof SimpleName sn) { if (expr instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding paramVb && paramBindings.containsKey(paramVb)) {
Expression argExpr = paramBindings.get(paramVb);
List<Expression> resolved = getReachingDefinitions(argExpr, visited, paramBindings, depth + 1);
visited.remove(expr);
return resolved;
}
// Fallback: Local Reaching Definitions
MethodDeclaration md = findEnclosingMethod(sn); MethodDeclaration md = findEnclosingMethod(sn);
ReachingDefinitions rd = getReachingDefinitionsForMethod(md); ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
if (rd != null) { if (rd != null) {
@@ -49,7 +93,7 @@ public class JdtDataFlowModel implements DataFlowModel {
for (ASTNode def : defs) { for (ASTNode def : defs) {
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def); Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
if (defExpr != null) { if (defExpr != null) {
exprs.addAll(getReachingDefinitions(defExpr, visited)); exprs.addAll(getReachingDefinitions(defExpr, visited, paramBindings, depth + 1));
} else { } else {
exprs.add(expr); 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<IVariableBinding, Expression> 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<ReturnStatement> returns = findReturnStatements(md.getBody());
if (!returns.isEmpty()) {
List<Expression> 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); visited.remove(expr);
return List.of(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<ReturnStatement> findReturnStatements(ASTNode node) {
List<ReturnStatement> 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 @Override
public String resolveValue(Expression expr, CodebaseContext context) { public String resolveValue(Expression expr, CodebaseContext context) {
if (expr == null) return null; if (expr == null) return null;

View File

@@ -38,7 +38,10 @@ public class ReachingDefinitions {
} }
boolean changed = true; boolean changed = true;
while (changed) { int iterations = 0;
int maxIterations = 5000;
while (changed && iterations < maxIterations) {
iterations++;
changed = false; changed = false;
for (ControlFlowGraph.CfgNode node : cfgNodes) { for (ControlFlowGraph.CfgNode node : cfgNodes) {
Set<ASTNode> newIn = new HashSet<>(); Set<ASTNode> 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<ASTNode> getReachingDefinitions(ASTNode usageNode, String varName) { public Set<ASTNode> getReachingDefinitions(ASTNode usageNode, String varName) {

View File

@@ -88,4 +88,166 @@ class JdtDataFlowModelTest {
// Both branches assign "SAME", so it converges to "SAME" // Both branches assign "SAME", so it converges to "SAME"
assertThat(val).isEqualTo("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" ], "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderStatic", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : { "triggerPoint" : {
"event" : "EventBuilder.buildEvent()", "event" : "new FulfillEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService", "className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent", "methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
@@ -447,10 +447,14 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 13, "lineNumber" : 13,
"polymorphicEvents" : [ ] "polymorphicEvents" : [ "OrderEvents.FULFILL" ]
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : [ {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.FULFILLED",
"event" : "OrderEvents.FULFILL"
} ]
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",
@@ -466,7 +470,7 @@
}, },
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : { "triggerPoint" : {
"event" : "new EventBuilder().buildInstanceEvent()", "event" : "new CancelEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService", "className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent", "methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
@@ -474,10 +478,14 @@
"stateMachineId" : null, "stateMachineId" : null,
"sourceState" : null, "sourceState" : null,
"lineNumber" : 13, "lineNumber" : 13,
"polymorphicEvents" : [ ] "polymorphicEvents" : [ "OrderEvents.CANCEL" ]
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null "matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.CANCEL"
} ]
}, { }, {
"entryPoint" : { "entryPoint" : {
"type" : "REST", "type" : "REST",