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 4a92150..41a060d 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 @@ -7,6 +7,7 @@ public class JdtDataFlowModel implements DataFlowModel { private final CodebaseContext context; private final Map cfgCache = new HashMap<>(); private final Map rdCache = new HashMap<>(); + private final Map> objectFieldBindings = new HashMap<>(); public JdtDataFlowModel(CodebaseContext context) { this.context = context; @@ -112,6 +113,13 @@ public class JdtDataFlowModel implements DataFlowModel { } } + // Handle ClassInstanceCreation + if (expr instanceof ClassInstanceCreation cic) { + objectFieldBindings.computeIfAbsent(cic, k -> buildFieldBindingsFromCic(cic, paramBindings, instanceFieldBindings)); + visited.remove(expr); + return List.of(cic); + } + // Handle CastExpression if (expr instanceof CastExpression ce) { List resolved = getReachingDefinitions(ce.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1); @@ -148,11 +156,30 @@ public class JdtDataFlowModel implements DataFlowModel { } } + // Evaluate mutator/setter method body + evaluateMethodMutations(md, mi.arguments(), target.fieldBindings); + List returns = findReturnStatements(md.getBody()); if (!returns.isEmpty()) { for (ReturnStatement rs : returns) { if (rs.getExpression() != null) { - results.addAll(getReachingDefinitions(rs.getExpression(), visited, newParamBindings, target.fieldBindings, depth + 1)); + Expression retExpr = rs.getExpression(); + Expression unwrapped = retExpr; + while (unwrapped instanceof ParenthesizedExpression pe) { + unwrapped = pe.getExpression(); + } + while (unwrapped instanceof CastExpression ce) { + unwrapped = ce.getExpression(); + } + if (unwrapped instanceof ThisExpression) { + if (mi.getExpression() != null) { + results.addAll(getReachingDefinitions(mi.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1)); + } else { + results.add(unwrapped); + } + } else { + results.addAll(getReachingDefinitions(retExpr, visited, newParamBindings, target.fieldBindings, depth + 1)); + } } } } @@ -284,11 +311,29 @@ public class JdtDataFlowModel implements DataFlowModel { } private Map buildFieldBindingsFromCic(ClassInstanceCreation cic) { + return buildFieldBindingsFromCic(cic, new HashMap<>(), new HashMap<>()); + } + + private Map buildFieldBindingsFromCic( + ClassInstanceCreation cic, + Map callerParamBindings, + Map callerInstanceFieldBindings) { Map fieldBindings = new HashMap<>(); IMethodBinding cb = cic.resolveConstructorBinding(); if (cb == null) return fieldBindings; - evaluateConstructor(cb, cic.arguments(), new HashMap<>(), fieldBindings, 0); + List resolvedArguments = new ArrayList<>(); + for (Object argObj : cic.arguments()) { + Expression arg = (Expression) argObj; + List resolved = getReachingDefinitions(arg, new HashSet<>(), callerParamBindings, callerInstanceFieldBindings, 0); + if (!resolved.isEmpty()) { + resolvedArguments.add(resolved.get(0)); + } else { + resolvedArguments.add(arg); + } + } + + evaluateConstructor(cb, resolvedArguments, new HashMap<>(), fieldBindings, 0); return fieldBindings; } @@ -426,7 +471,26 @@ public class JdtDataFlowModel implements DataFlowModel { if (concreteType != null) { MethodDeclaration md = findMethodDeclarationInType(concreteType, mb); if (md != null) { - Map concreteFieldBindings = buildFieldBindingsFromCic(cic); + Map concreteFieldBindings = + objectFieldBindings.computeIfAbsent(cic, k -> buildFieldBindingsFromCic((ClassInstanceCreation) k)); + + // Apply flow-sensitive intermediate local variable mutations + if (receiver instanceof SimpleName sn) { + MethodDeclaration enclosingMethod = findEnclosingMethod(sn); + if (enclosingMethod != null) { + ReachingDefinitions rd = getReachingDefinitionsForMethod(enclosingMethod); + if (rd != null) { + Set defs = rd.getReachingDefinitions(sn, sn.getIdentifier()); + for (ASTNode def : defs) { + Expression defExpr = ReachingDefinitions.getDefinitionExpression(def); + if (defExpr == cic || isExpressionWrapping(defExpr, cic)) { + applyIntermediateMutations(sn, def, mi, enclosingMethod, concreteFieldBindings, paramBindings, visited, depth); + } + } + } + } + } + targets.add(new TargetMethod(md, concreteFieldBindings)); } } @@ -456,6 +520,23 @@ public class JdtDataFlowModel implements DataFlowModel { } } } + + // If the declaring class is a class (not an interface), or if the declared method itself is concrete/has a body, + // we should also include the declared method itself in the polymorphic targets. + MethodDeclaration declaredMd = findMethodDeclaration(mb); + if (declaredMd != null && declaredMd.getBody() != null) { + boolean alreadyAdded = false; + for (TargetMethod tm : targets) { + if (tm.declaration == declaredMd) { + alreadyAdded = true; + break; + } + } + if (!alreadyAdded) { + Map targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>(); + targets.add(new TargetMethod(declaredMd, targetFieldBindings)); + } + } } // If no polymorphic subclass targets were found, fallback to the declared method itself @@ -470,6 +551,218 @@ public class JdtDataFlowModel implements DataFlowModel { return targets; } + private boolean isExpressionWrapping(Expression outer, Expression inner) { + Expression current = outer; + while (current != null) { + if (current == inner) return true; + if (current instanceof ParenthesizedExpression pe) { + current = pe.getExpression(); + } else if (current instanceof CastExpression ce) { + current = ce.getExpression(); + } else { + break; + } + } + return false; + } + + private ControlFlowGraph.CfgNode findCfgNode(ControlFlowGraph cfg, ASTNode astNode) { + ASTNode current = astNode; + while (current != null) { + for (ControlFlowGraph.CfgNode node : cfg.getNodes()) { + if (node.getAstNode() == current) { + return node; + } + } + current = current.getParent(); + } + return null; + } + + private boolean isTargetVariable(Expression expr, IVariableBinding targetVar, String targetName) { + if (expr instanceof SimpleName sn) { + IBinding b = sn.resolveBinding(); + if (b instanceof IVariableBinding vb) { + if (targetVar != null && vb.getKey().equals(targetVar.getKey())) { + return true; + } + } + if (targetName != null && sn.getIdentifier().equals(targetName)) { + return true; + } + } + return false; + } + + private void evaluateMethodMutations( + MethodDeclaration md, + List arguments, + Map fieldBindings) { + if (md == null || md.getBody() == null) return; + + Map currentParamValues = new HashMap<>(); + List parameters = md.parameters(); + 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) { + Expression arg = (Expression) arguments.get(i); + currentParamValues.put(paramVb, arg); + } + } + + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(Assignment assignment) { + Expression lhs = assignment.getLeftHandSide(); + Expression rhs = assignment.getRightHandSide(); + IVariableBinding fieldVb = null; + if (lhs instanceof FieldAccess fa) { + if (fa.getExpression() instanceof ThisExpression) { + IBinding b = fa.getName().resolveBinding(); + if (b instanceof IVariableBinding vb && vb.isField()) { + fieldVb = vb; + } + } + } else if (lhs instanceof SimpleName sn) { + IBinding b = sn.resolveBinding(); + if (b instanceof IVariableBinding vb && vb.isField()) { + fieldVb = vb; + } + } + + if (fieldVb != null) { + Expression resolvedRhs = resolveParamValue(rhs, currentParamValues); + fieldBindings.put(fieldVb, resolvedRhs); + } + return super.visit(assignment); + } + }); + } + + private void findAndApplyMutations( + ASTNode node, + IVariableBinding targetVar, + String targetName, + Map fieldBindings, + Map paramBindings, + Set visited, + int depth) { + node.accept(new ASTVisitor() { + @Override + public boolean visit(Assignment assignment) { + Expression lhs = assignment.getLeftHandSide(); + Expression rhs = assignment.getRightHandSide(); + IVariableBinding fieldVb = null; + if (lhs instanceof FieldAccess fa) { + if (isTargetVariable(fa.getExpression(), targetVar, targetName)) { + IBinding b = fa.getName().resolveBinding(); + if (b instanceof IVariableBinding vb && vb.isField()) { + fieldVb = vb; + } + } + } else if (lhs instanceof QualifiedName qn) { + if (isTargetVariable(qn.getQualifier(), targetVar, targetName)) { + IBinding b = qn.getName().resolveBinding(); + if (b instanceof IVariableBinding vb && vb.isField()) { + fieldVb = vb; + } + } + } + + if (fieldVb != null) { + List resolvedRhs = getReachingDefinitions(rhs, visited, paramBindings, fieldBindings, depth + 1); + if (!resolvedRhs.isEmpty()) { + fieldBindings.put(fieldVb, resolvedRhs.get(0)); + } else { + fieldBindings.put(fieldVb, rhs); + } + } + return super.visit(assignment); + } + + @Override + public boolean visit(MethodInvocation mi) { + Expression receiver = mi.getExpression(); + if (receiver != null && isTargetVariable(receiver, targetVar, targetName)) { + IMethodBinding mb = mi.resolveMethodBinding(); + if (mb != null) { + List targets = resolveTargets(mi, mb, visited, paramBindings, fieldBindings, depth + 1); + for (TargetMethod target : targets) { + if (target.declaration != null) { + evaluateMethodMutations(target.declaration, mi.arguments(), fieldBindings); + } + } + } + } + return super.visit(mi); + } + }); + } + + private void applyIntermediateMutations( + SimpleName receiverSimpleName, + ASTNode defNode, + ASTNode useNode, + MethodDeclaration enclosingMethod, + Map fieldBindings, + Map paramBindings, + Set visited, + int depth) { + + IBinding b = receiverSimpleName.resolveBinding(); + IVariableBinding targetVar = (b instanceof IVariableBinding vb) ? vb : null; + String targetName = receiverSimpleName.getIdentifier(); + + ControlFlowGraph cfg = cfgCache.computeIfAbsent(enclosingMethod, CfgBuilder::build); + if (cfg == null) return; + + ControlFlowGraph.CfgNode defCfgNode = findCfgNode(cfg, defNode); + ControlFlowGraph.CfgNode useCfgNode = findCfgNode(cfg, useNode); + + if (defCfgNode != null && useCfgNode != null) { + Set reachFromD = new HashSet<>(); + Queue queue = new LinkedList<>(); + queue.add(defCfgNode); + reachFromD.add(defCfgNode); + while (!queue.isEmpty()) { + ControlFlowGraph.CfgNode curr = queue.poll(); + for (ControlFlowGraph.CfgNode succ : curr.getSuccessors()) { + if (reachFromD.add(succ)) { + queue.add(succ); + } + } + } + + Set reachToU = new HashSet<>(); + queue.clear(); + queue.add(useCfgNode); + reachToU.add(useCfgNode); + while (!queue.isEmpty()) { + ControlFlowGraph.CfgNode curr = queue.poll(); + for (ControlFlowGraph.CfgNode pred : curr.getPredecessors()) { + if (reachToU.add(pred)) { + queue.add(pred); + } + } + } + + Set pathNodes = new HashSet<>(reachFromD); + pathNodes.retainAll(reachToU); + + List orderedPathNodes = new ArrayList<>(pathNodes); + List allNodes = cfg.getNodes(); + orderedPathNodes.sort(Comparator.comparingInt(allNodes::indexOf)); + + for (ControlFlowGraph.CfgNode node : orderedPathNodes) { + if (node != defCfgNode && node != useCfgNode && node.getAstNode() != null) { + findAndApplyMutations(node.getAstNode(), targetVar, targetName, fieldBindings, paramBindings, visited, depth); + } + } + } + } + private MethodDeclaration findMethodDeclarationInType(ITypeBinding typeBinding, IMethodBinding mb) { if (typeBinding == null || mb == null) return null; 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 08d1f55..c5cae49 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 @@ -512,5 +512,166 @@ class JdtDataFlowModelTest { String val = model.resolveValue(initEv, context); assertThat(val).isEqualTo("PAY"); } + + @Test + void testFlowSensitiveLocalFieldMutations(@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 class Command {\n" + + " private String event = \"DEFAULT\";\n" + + " public void setEvent(String event) {\n" + + " this.event = event;\n" + + " }\n" + + " public String getEvent() {\n" + + " return this.event;\n" + + " }\n" + + " }\n" + + " public void run() {\n" + + " Command cmd = new Command();\n" + + " cmd.setEvent(\"PAY\");\n" + + " String res = cmd.getEvent();\n" + + " }\n" + + "}" + ); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.setSourcepath(List.of(tempDir.toAbsolutePath().toString())); + context.scan(tempDir); + + TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); + assertThat(td).isNotNull(); + + MethodDeclaration md = null; + for (MethodDeclaration m : td.getMethods()) { + if (m.getName().getIdentifier().equals("run")) { + md = m; + } + } + assertThat(md).isNotNull(); + + VariableDeclarationStatement resStmt = (VariableDeclarationStatement) md.getBody().statements().get(2); + VariableDeclarationFragment resFrag = (VariableDeclarationFragment) resStmt.fragments().get(0); + Expression initializer = resFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + String val = model.resolveValue(initializer, context); + assertThat(val).isEqualTo("PAY"); + } + + @Test + void testFluentBuilderPatternPropagation(@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 class EventWrapper {\n" + + " private final String event;\n" + + " private EventWrapper(String event) {\n" + + " this.event = event;\n" + + " }\n" + + " public String getEvent() { return this.event; }\n" + + " public static Builder builder() {\n" + + " return new Builder();\n" + + " }\n" + + " public static class Builder {\n" + + " private String event;\n" + + " public Builder event(String event) {\n" + + " this.event = event;\n" + + " return this;\n" + + " }\n" + + " public EventWrapper build() {\n" + + " return new EventWrapper(this.event);\n" + + " }\n" + + " }\n" + + " }\n" + + " public void run() {\n" + + " EventWrapper w = EventWrapper.builder().event(\"PAY_SUCCESS\").build();\n" + + " String res = w.getEvent();\n" + + " }\n" + + "}" + ); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.setSourcepath(List.of(tempDir.toAbsolutePath().toString())); + context.scan(tempDir); + + TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); + assertThat(td).isNotNull(); + + MethodDeclaration md = null; + for (MethodDeclaration m : td.getMethods()) { + if (m.getName().getIdentifier().equals("run")) { + md = m; + } + } + assertThat(md).isNotNull(); + + VariableDeclarationStatement resStmt = (VariableDeclarationStatement) md.getBody().statements().get(1); + VariableDeclarationFragment resFrag = (VariableDeclarationFragment) resStmt.fragments().get(0); + Expression initializer = resFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + String val = model.resolveValue(initializer, context); + assertThat(val).isEqualTo("PAY_SUCCESS"); + } + + @Test + void testPolymorphicUnionFallback(@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 interface Service {\n" + + " String getEvent();\n" + + " }\n" + + " public static class PayService implements Service {\n" + + " public String getEvent() { return \"PAY\"; }\n" + + " }\n" + + " public static class CancelService implements Service {\n" + + " public String getEvent() { return \"CANCEL\"; }\n" + + " }\n" + + " public void run(Service s) {\n" + + " String ev = s.getEvent();\n" + + " }\n" + + "}" + ); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.setSourcepath(List.of(tempDir.toAbsolutePath().toString())); + context.scan(tempDir); + + TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); + assertThat(td).isNotNull(); + + MethodDeclaration md = null; + for (MethodDeclaration m : td.getMethods()) { + if (m.getName().getIdentifier().equals("run")) { + md = m; + } + } + assertThat(md).isNotNull(); + + VariableDeclarationStatement evStmt = (VariableDeclarationStatement) md.getBody().statements().get(0); + VariableDeclarationFragment evFrag = (VariableDeclarationFragment) evStmt.fragments().get(0); + Expression initEv = evFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + List defs = model.getReachingDefinitions(initEv); + assertThat(defs).hasSize(2); + + List values = defs.stream() + .filter(e -> e instanceof StringLiteral) + .map(e -> ((StringLiteral) e).getLiteralValue()) + .toList(); + assertThat(values).containsExactlyInAnyOrder("PAY", "CANCEL"); + } }