From a7c3b081649d85c5ecbdfdb6e080b65fa076302d Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sat, 27 Jun 2026 19:51:26 +0200 Subject: [PATCH] stage 3 --- .../ast/common/JdtDataFlowModel.java | 376 ++++++++++++++++-- .../ast/common/JdtDataFlowModelTest.java | 263 ++++++++++++ .../PolymorphicStateMachineConfiguration.json | 2 +- 3 files changed, 611 insertions(+), 30 deletions(-) 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 9845c36..4a92150 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,14 @@ public class JdtDataFlowModel implements DataFlowModel { @Override public List getReachingDefinitions(Expression expr) { - return getReachingDefinitions(expr, new HashSet<>(), new HashMap<>(), 0); + return getReachingDefinitions(expr, new HashSet<>(), new HashMap<>(), new HashMap<>(), 0); } private List getReachingDefinitions( Expression expr, Set visited, Map paramBindings, + Map instanceFieldBindings, int depth) { if (expr == null || depth > 50 || !visited.add(expr)) { return List.of(); @@ -49,11 +50,11 @@ public class JdtDataFlowModel implements DataFlowModel { if (expr instanceof ConditionalExpression ce) { String condVal = resolveValue(ce.getExpression(), context); if ("true".equals(condVal)) { - List resolved = getReachingDefinitions(ce.getThenExpression(), visited, paramBindings, depth + 1); + List resolved = getReachingDefinitions(ce.getThenExpression(), visited, paramBindings, instanceFieldBindings, depth + 1); visited.remove(expr); return resolved; } else if ("false".equals(condVal)) { - List resolved = getReachingDefinitions(ce.getElseExpression(), visited, paramBindings, depth + 1); + List resolved = getReachingDefinitions(ce.getElseExpression(), visited, paramBindings, instanceFieldBindings, depth + 1); visited.remove(expr); return resolved; } else { @@ -65,9 +66,16 @@ public class JdtDataFlowModel implements DataFlowModel { // 2. Handle Field constant / field initializer propagation IVariableBinding vb = getVariableBinding(expr); if (vb != null && vb.isField()) { + if (instanceFieldBindings.containsKey(vb)) { + Expression fieldVal = instanceFieldBindings.get(vb); + List resolved = getReachingDefinitions(fieldVal, visited, paramBindings, instanceFieldBindings, depth + 1); + visited.remove(expr); + return resolved; + } + Expression fieldInit = findFieldInitializer(vb, expr); if (fieldInit != null) { - List resolved = getReachingDefinitions(fieldInit, visited, paramBindings, depth + 1); + List resolved = getReachingDefinitions(fieldInit, visited, paramBindings, instanceFieldBindings, depth + 1); visited.remove(expr); return resolved; } @@ -78,7 +86,7 @@ public class JdtDataFlowModel implements DataFlowModel { 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); + List resolved = getReachingDefinitions(argExpr, visited, paramBindings, instanceFieldBindings, depth + 1); visited.remove(expr); return resolved; } @@ -93,7 +101,7 @@ public class JdtDataFlowModel implements DataFlowModel { for (ASTNode def : defs) { Expression defExpr = ReachingDefinitions.getDefinitionExpression(def); if (defExpr != null) { - exprs.addAll(getReachingDefinitions(defExpr, visited, paramBindings, depth + 1)); + exprs.addAll(getReachingDefinitions(defExpr, visited, paramBindings, instanceFieldBindings, depth + 1)); } else { exprs.add(expr); } @@ -104,15 +112,69 @@ public class JdtDataFlowModel implements DataFlowModel { } } + // Handle CastExpression + if (expr instanceof CastExpression ce) { + List resolved = getReachingDefinitions(ce.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1); + visited.remove(expr); + return resolved; + } + + // Handle ParenthesizedExpression + if (expr instanceof ParenthesizedExpression pe) { + List resolved = getReachingDefinitions(pe.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1); + visited.remove(expr); + return resolved; + } + // 4. Handle Method Invocations (Static Factory methods / Transforming Getters) if (expr instanceof MethodInvocation mi) { IMethodBinding mb = mi.resolveMethodBinding(); + if (mb != null) { + List targets = resolveTargets(mi, mb, visited, paramBindings, instanceFieldBindings, depth); + if (!targets.isEmpty()) { + List results = new ArrayList<>(); + for (TargetMethod target : targets) { + MethodDeclaration md = target.declaration; + 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()) { + for (ReturnStatement rs : returns) { + if (rs.getExpression() != null) { + results.addAll(getReachingDefinitions(rs.getExpression(), visited, newParamBindings, target.fieldBindings, depth + 1)); + } + } + } + } + } + if (!results.isEmpty()) { + visited.remove(expr); + return results; + } + } + } + } + + // Handle Super Method Invocations + if (expr instanceof SuperMethodInvocation smi) { + IMethodBinding mb = smi.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(); + List arguments = smi.arguments(); int count = Math.min(parameters.size(), arguments.size()); for (int i = 0; i < count; i++) { SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i); @@ -127,7 +189,7 @@ public class JdtDataFlowModel implements DataFlowModel { List results = new ArrayList<>(); for (ReturnStatement rs : returns) { if (rs.getExpression() != null) { - results.addAll(getReachingDefinitions(rs.getExpression(), visited, newParamBindings, depth + 1)); + results.addAll(getReachingDefinitions(rs.getExpression(), visited, newParamBindings, instanceFieldBindings, depth + 1)); } } visited.remove(expr); @@ -174,41 +236,297 @@ public class JdtDataFlowModel implements DataFlowModel { } if (td != null) { + // 1. Try field declaration initializer 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(); + if (fragment.getInitializer() != null) { + return fragment.getInitializer(); + } } } } } + // 2. Try constructor assignments + for (MethodDeclaration md : td.getMethods()) { + if (md.isConstructor() && md.getBody() != null) { + final Expression[] assignedExpr = new Expression[1]; + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(Assignment assignment) { + Expression lhs = assignment.getLeftHandSide(); + boolean matches = false; + if (lhs instanceof SimpleName snLhs && snLhs.getIdentifier().equals(vb.getName())) { + IBinding b = snLhs.resolveBinding(); + if (b instanceof IVariableBinding resolvedVb && resolvedVb.getKey().equals(vb.getKey())) { + matches = true; + } + } else if (lhs instanceof FieldAccess fa && fa.getName().getIdentifier().equals(vb.getName()) && fa.getExpression() instanceof ThisExpression) { + IBinding b = fa.getName().resolveBinding(); + if (b instanceof IVariableBinding resolvedVb && resolvedVb.getKey().equals(vb.getKey())) { + matches = true; + } + } + if (matches) { + assignedExpr[0] = assignment.getRightHandSide(); + } + return super.visit(assignment); + } + }); + if (assignedExpr[0] != null) { + return assignedExpr[0]; + } + } + } + } + return null; + } + + private Map buildFieldBindingsFromCic(ClassInstanceCreation cic) { + Map fieldBindings = new HashMap<>(); + IMethodBinding cb = cic.resolveConstructorBinding(); + if (cb == null) return fieldBindings; + + evaluateConstructor(cb, cic.arguments(), new HashMap<>(), fieldBindings, 0); + return fieldBindings; + } + + private void evaluateConstructor( + IMethodBinding cb, + List arguments, + Map callerParamValues, + Map fieldBindings, + int depth) { + if (cb == null || depth > 10) return; + MethodDeclaration cd = findMethodDeclaration(cb); + if (cd == null || cd.getBody() == null) return; + + Map currentParamValues = new HashMap<>(); + List parameters = cd.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); + Expression resolvedArg = resolveParamValue(arg, callerParamValues); + currentParamValues.put(paramVb, resolvedArg); + } + } + + List statements = cd.getBody().statements(); + if (!statements.isEmpty()) { + Object firstStmt = statements.get(0); + if (firstStmt instanceof ConstructorInvocation ci) { + IMethodBinding targetCb = ci.resolveConstructorBinding(); + if (targetCb != null) { + evaluateConstructor(targetCb, ci.arguments(), currentParamValues, fieldBindings, depth + 1); + } + } else if (firstStmt instanceof SuperConstructorInvocation sci) { + IMethodBinding targetCb = sci.resolveConstructorBinding(); + if (targetCb != null) { + evaluateConstructor(targetCb, sci.arguments(), currentParamValues, fieldBindings, depth + 1); + } + } + } + + cd.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 Expression resolveParamValue(Expression expr, Map paramValues) { + if (expr instanceof SimpleName sn) { + IBinding b = sn.resolveBinding(); + if (b instanceof IVariableBinding vb && paramValues.containsKey(vb)) { + return paramValues.get(vb); + } + } else if (expr instanceof CastExpression ce) { + Expression resolved = resolveParamValue(ce.getExpression(), paramValues); + if (resolved != ce.getExpression()) { + return resolved; + } + } else if (expr instanceof ParenthesizedExpression pe) { + Expression resolved = resolveParamValue(pe.getExpression(), paramValues); + if (resolved != pe.getExpression()) { + return resolved; + } + } + return expr; + } + + private static class TargetMethod { + final MethodDeclaration declaration; + final Map fieldBindings; + + TargetMethod(MethodDeclaration declaration, Map fieldBindings) { + this.declaration = declaration; + this.fieldBindings = fieldBindings; + } + } + + private List resolveTargets( + MethodInvocation mi, + IMethodBinding mb, + Set visited, + Map paramBindings, + Map instanceFieldBindings, + int depth) { + + Expression receiver = mi.getExpression(); + boolean isThisReceiver = (receiver == null || receiver instanceof ThisExpression); + + List targets = new ArrayList<>(); + + if (receiver != null) { + // Find concrete ClassInstanceCreation definitions in the reaching definitions path of the receiver + List receiverDefs = getReachingDefinitions(receiver, visited, paramBindings, instanceFieldBindings, depth + 1); + List concreteCics = new ArrayList<>(); + for (Expression rDef : receiverDefs) { + if (rDef instanceof ClassInstanceCreation cic) { + concreteCics.add(cic); + } + } + + if (!concreteCics.isEmpty()) { + // Type narrowing is active! Only trace through the concrete instantiated types. + for (ClassInstanceCreation cic : concreteCics) { + ITypeBinding concreteType = null; + if (cic.resolveConstructorBinding() != null) { + concreteType = cic.resolveConstructorBinding().getDeclaringClass(); + } + if (concreteType == null) { + concreteType = cic.resolveTypeBinding(); + } + if (concreteType != null) { + MethodDeclaration md = findMethodDeclarationInType(concreteType, mb); + if (md != null) { + Map concreteFieldBindings = buildFieldBindingsFromCic(cic); + targets.add(new TargetMethod(md, concreteFieldBindings)); + } + } + } + return targets; + } + } + + // Fallback: Polymorphic target discovery (including interface implementations) + ITypeBinding declaringClass = mb.getDeclaringClass(); + if (declaringClass != null) { + String declaringClassFqn = declaringClass.getErasure().getQualifiedName(); + if (declaringClassFqn != null && !declaringClassFqn.isEmpty()) { + // Find all known implementation subclasses in the codebase + List implClassFqns = context.getImplementations(declaringClassFqn); + if (implClassFqns != null && !implClassFqns.isEmpty()) { + for (String implFqn : implClassFqns) { + TypeDeclaration td = context.getTypeDeclaration(implFqn); + if (td != null) { + MethodDeclaration md = findMethodDeclarationInType(td.resolveBinding(), mb); + if (md != null) { + // For polymorphic targets with unknown instance, we don't have concrete field bindings + Map targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>(); + targets.add(new TargetMethod(md, targetFieldBindings)); + } + } + } + } + } + } + + // If no polymorphic subclass targets were found, fallback to the declared method itself + if (targets.isEmpty()) { + MethodDeclaration md = findMethodDeclaration(mb); + if (md != null) { + Map targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>(); + targets.add(new TargetMethod(md, targetFieldBindings)); + } + } + + return targets; + } + + private MethodDeclaration findMethodDeclarationInType(ITypeBinding typeBinding, IMethodBinding mb) { + if (typeBinding == null || mb == null) return null; + + // 1. Try to find it in the class hierarchy (including typeBinding itself) + ITypeBinding current = typeBinding; + while (current != null) { + MethodDeclaration md = findMethodInTypeDeclarationOnly(current, mb); + if (md != null) return md; + current = current.getSuperclass(); + } + + // 2. Try to find it in the interface hierarchy of typeBinding + Set visited = new HashSet<>(); + Queue queue = new LinkedList<>(); + queue.add(typeBinding); + while (!queue.isEmpty()) { + ITypeBinding tb = queue.poll(); + if (tb == null) continue; + String key = tb.getKey(); + if (key != null && !visited.add(key)) continue; + + MethodDeclaration md = findMethodInTypeDeclarationOnly(tb, mb); + if (md != null) return md; + + for (ITypeBinding interf : tb.getInterfaces()) { + queue.add(interf); + } + } + + return null; + } + + private MethodDeclaration findMethodInTypeDeclarationOnly(ITypeBinding typeBinding, IMethodBinding mb) { + String classFqn = typeBinding.getErasure().getQualifiedName(); + if (classFqn != null && !classFqn.isEmpty()) { + 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 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; + return findMethodDeclarationInType(mb.getDeclaringClass(), mb); } private List findReturnStatements(ASTNode node) { 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 100951e..08d1f55 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 @@ -104,6 +104,8 @@ class JdtDataFlowModelTest { ); CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.setSourcepath(List.of(tempDir.toAbsolutePath().toString())); context.scan(tempDir); TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); @@ -134,6 +136,8 @@ class JdtDataFlowModelTest { ); CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.setSourcepath(List.of(tempDir.toAbsolutePath().toString())); context.scan(tempDir); TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); @@ -179,6 +183,8 @@ class JdtDataFlowModelTest { ); CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.setSourcepath(List.of(tempDir.toAbsolutePath().toString())); context.scan(tempDir); TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); @@ -219,6 +225,8 @@ class JdtDataFlowModelTest { ); CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.setSourcepath(List.of(tempDir.toAbsolutePath().toString())); context.scan(tempDir); TypeDeclaration td = context.getTypeDeclaration("com.foo.Test"); @@ -250,4 +258,259 @@ class JdtDataFlowModelTest { } assertThat(hasLoopBackToBody).isTrue(); } + + @Test + void testConstructorAssignmentPropagation(@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 final String myVar;\n" + + " public Test() {\n" + + " this.myVar = \"CONSTRUCTOR_VAL\";\n" + + " }\n" + + " public void run() {\n" + + " String a = myVar;\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 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("CONSTRUCTOR_VAL"); + } + + @Test + void testObjectSensitiveInstanceFieldTracing(@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 Builder {\n" + + " private final String val;\n" + + " public Builder(String val) {\n" + + " this.val = val;\n" + + " }\n" + + " public String getVal() {\n" + + " return this.val;\n" + + " }\n" + + " }\n" + + " public void run() {\n" + + " Builder b = new Builder(\"OBJECT_SENSITIVE\");\n" + + " String res = b.getVal();\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(); // b.getVal() + + DataFlowModel model = new JdtDataFlowModel(context); + String val = model.resolveValue(initializer, context); + assertThat(val).isEqualTo("OBJECT_SENSITIVE"); + } + + @Test + void testConstructorChaining(@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 final String event;\n" + + " private final String info;\n" + + " public Command(String event, String info) {\n" + + " this.event = event;\n" + + " this.info = info;\n" + + " }\n" + + " public Command(String event) {\n" + + " this(event, \"DEFAULT_INFO\");\n" + + " }\n" + + " public Command() {\n" + + " this(\"DEFAULT_EVENT\");\n" + + " }\n" + + " public String getEvent() { return this.event; }\n" + + " public String getInfo() { return this.info; }\n" + + " }\n" + + " public void run() {\n" + + " Command cmd = new Command();\n" + + " String ev = cmd.getEvent();\n" + + " String inf = cmd.getInfo();\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(1); + VariableDeclarationFragment evFrag = (VariableDeclarationFragment) evStmt.fragments().get(0); + Expression initEv = evFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + String valEv = model.resolveValue(initEv, context); + assertThat(valEv).isEqualTo("DEFAULT_EVENT"); + + VariableDeclarationStatement infStmt = (VariableDeclarationStatement) md.getBody().statements().get(2); + VariableDeclarationFragment infFrag = (VariableDeclarationFragment) infStmt.fragments().get(0); + Expression initInf = infFrag.getInitializer(); + + String valInf = model.resolveValue(initInf, context); + assertThat(valInf).isEqualTo("DEFAULT_INFO"); + } + + @Test + void testExplicitSuperCalls(@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 Base {\n" + + " public String getValue() {\n" + + " return \"BASE_VAL\";\n" + + " }\n" + + " }\n" + + " public static class Sub extends Base {\n" + + " @Override\n" + + " public String getValue() {\n" + + " return super.getValue();\n" + + " }\n" + + " }\n" + + " public void run() {\n" + + " Sub sub = new Sub();\n" + + " String val = sub.getValue();\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 valStmt = (VariableDeclarationStatement) md.getBody().statements().get(1); + VariableDeclarationFragment valFrag = (VariableDeclarationFragment) valStmt.fragments().get(0); + Expression initVal = valFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + String val = model.resolveValue(initVal, context); + assertThat(val).isEqualTo("BASE_VAL"); + } + + @Test + void testDynamicDispatchNarrowing(@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() {\n" + + " Service s = new PayService();\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(1); + VariableDeclarationFragment evFrag = (VariableDeclarationFragment) evStmt.fragments().get(0); + Expression initEv = evFrag.getInitializer(); + + DataFlowModel model = new JdtDataFlowModel(context); + List defs = model.getReachingDefinitions(initEv); + assertThat(defs).isNotEmpty(); + + String val = model.resolveValue(initEv, context); + assertThat(val).isEqualTo("PAY"); + } } + 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 d94b499..490e359 100644 --- a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json +++ b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json @@ -338,7 +338,7 @@ }, "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payCast", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], "triggerPoint" : { - "event" : "(BaseEvent)new PayEvent()", + "event" : "new PayEvent()", "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", "methodName" : "processEvent", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",