From 4511e0e02fd380b48720d3d69ee38335708e2552 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sat, 27 Jun 2026 20:03:48 +0200 Subject: [PATCH] stage next --- plan-extended-anaylis/PLAN.md | 81 ++--- .../ast/common/JdtDataFlowModel.java | 299 +++++++++++++++++- .../ast/common/JdtDataFlowModelTest.java | 161 ++++++++++ 3 files changed, 475 insertions(+), 66 deletions(-) diff --git a/plan-extended-anaylis/PLAN.md b/plan-extended-anaylis/PLAN.md index bdaebbe..d9039e0 100644 --- a/plan-extended-anaylis/PLAN.md +++ b/plan-extended-anaylis/PLAN.md @@ -1,71 +1,26 @@ -# Execution Plan for Extended Analysis (Revised for Robustness) -## Phase 0: Surgical Refactoring (The Enrichment Hook) -- [x] Create `AnalysisResult` and `CodebaseMetadata` models. -- [x] Update `ExportService.generateOutputs` to accept `AnalysisResult`. -- [x] Implement `EnrichmentService` and `AnalysisEnricher` interface. -- [x] Wrap current parser outputs in `AnalysisResult` within `ExportService`. +## Phase 11: Formal Compiler & Static Analysis Alignment (Limitations & Missing Features) +While the custom AST-based `JdtDataFlowModel` supports advanced OOP data flow (like flow-sensitive local mutations, fluent builders, and polymorphic union fallbacks), the following compiler/static analysis behaviors are missing or simplified: -## Phase 1: Foundation & Semantic Core -- [ ] **JDT Binding Resolution**: Configure `ASTParser` with project classpath and `setResolveBindings(true)`. -- [ ] **Workspace Scoping**: Implement scanner with configurable exclude patterns (`test/`, `build/`, `node_modules/`). -- [ ] **ConstantResolver**: Implement lookup for `public static final` constants across class boundaries using JDT bindings. -- [ ] **Basic PropertyResolver**: Scan `application.properties/yml` for simple key-value pairs. -- [ ] Implement `JdtIntelligenceProvider` as the primary semantic analyzer. +1. **Full Pointer & Alias Analysis**: + - If two variables alias the same object (e.g., `Command cmd1 = cmd2;`), a mutation on `cmd1` (e.g., `cmd1.setEvent("A")`) is not reflected when reading the fields of `cmd2` unless they resolve to the exact same AST node reference. A formal pointer/alias analysis (like Anderson's or Steensgaard's algorithm) would map abstract heap locations rather than AST nodes. -## Phase 2: Trigger Discovery & Instance Identity -- [ ] **GenericEventDetector**: Find `sendEvent` calls using `ASTVisitor`. -- [ ] **InstanceIdentifier**: Detect which State Machine is being targeted (via `@Qualifier`, bean names, or generic types `StateMachine`). -- [ ] **Trigger Filter**: Link `sendEvent` calls to specific SM instances to avoid noise in multi-SM projects. -- [ ] Enhance `CodebaseContext` to provide mapping from `MethodDeclaration` to its callers using bindings. +2. **Advanced Loop Fixpoints for Fields**: + - If a loop mutates a field based on its previous values (e.g., `while(cond) { builder.count(builder.getCount() + 1); }`), a simple sequential pass will miss the cyclic dependency. A formal data flow framework uses a mathematical lattice and runs a fixpoint iteration algorithm (using meet/join operators) to compute the stable approximation of values. -## Phase 3: Entry Point Mapping -- [ ] **SpringMvcDetector**: Detect REST endpoints (using `ConstantResolver` for paths). -- [ ] **MessageListenerDetector**: Detect Kafka/JMS listeners (using `ConstantResolver` for topics/queues). -- [ ] **ConfigurableTriggerDetector**: Support user-defined patterns for custom frameworks. +3. **Exception Path Analysis (Exception CFG)**: + - Edges for exception flows (`throw` statements, `try-catch-finally` blocks, and implicit unchecked runtime exceptions like `NullPointerException`) are not modeled. Consequently, mutations or return paths within catch/finally blocks may be missed or evaluated out of order. -## Phase 4: Call Graph Integration -- [x] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls. -- [ ] Support inheritance in call graph resolution (using JDT bindings to resolve method overrides). +4. **Collection & Array Tracking**: + - Arrays and Collections (such as `List`, `Map`, `Set`) are treated as opaque objects, and their operations (like `list.add(val)` or `map.put(key, val)`) are evaluated as general method calls. Modifying a collection does not propagate the values to its getter calls (e.g. `list.get(0)`). -## Phase 4.5: Hierarchical Robustness (Inheritance Support) -- [ ] **Hierarchical Annotation Lookup**: Update `SpringMvcDetector` to scan interfaces and superclasses for inherited mapping annotations. -- [ ] **Interface Implementation Mapping**: Enhance `CodebaseContext` to index "Class -> Interfaces" and "Interface -> Classes" relationships. -- [ ] **Polymorphic Call Trace**: Update `CallGraphBuilder` to follow calls from an interface method to all known implementations in the project. -- [ ] **Base Class Trigger Discovery**: Ensure triggers in abstract base classes are correctly attributed to their concrete subclasses. +5. **Field Shadowing and Inheritance Semantics**: + - If a subclass shadows a superclass field (declaring a field with the same name), our resolver might resolve the wrong field binding. Also, implicit superclass constructors and field initializers from superclass hierarchies are not fully simulated. + +6. **Static Initialization Blocks and Static Mutable State**: + - Class loading execution order (i.e. static block initializations `static { ... }`) is not modeled. A mutation on a static field in one method is not propagated to reads in another method. + +7. **Type Erasure & Generics Precision**: + - Generics type bounds (e.g., wildcards `? extends T`, type parameter replacements) are not fully tracked, which can result in incorrect polymorphic resolution when method signatures differ slightly from erased bindings. -## Phase 5: Visualization & Diagnostics -- [ ] Update `ExportService` to include trigger information. -- [ ] Update DOT/SCXML exporters to show entry points. -- [ ] **Diagnostic Nodes**: Show "Potential/Unresolved Trigger" nodes when a `sendEvent` is found but the event name or SM instance cannot be resolved. - -## Phase 6: Validation (Milestone 1) -- [ ] Create a "Complex Sample Project" with REST controllers, services, and multiple state machines. -- [ ] Add unit tests for each detector and the aggregator. -- [ ] Verify that inheritance in both state machine config and controllers is correctly handled. - -## Phase 7: Advanced Resolution & Data Flow -- [ ] **Variable Propagation**: Track values from fields/constructors to their usage in the State Machine configuration (e.g., resolving `initial(initialState)` where `initialState` is a field). -- [ ] **@Value Resolution**: Link `@Value` annotations to the `PropertyResolver` to substitute placeholders with literal values in the diagram. -- [ ] **Spring Profile Support**: Update `PropertyResolver` to handle `application-{profile}.properties` and prioritize them based on an "active profiles" setting. -- [ ] **Complex ValueResolver**: Handle string concatenations (e.g., `"PREFIX_" + MyConstants.SUFFIX`) and variable references. -- [ ] **Payload Analysis**: Extract payload types from entry points and track field usage in `sendEvent` calls. - -## Phase 8: Persistence & Lifecycle Mapping -- [ ] Detect persistence restoration logic (`persister.restore`). -- [ ] Map "Resume" points where the machine state is loaded from a DB based on external identifiers. -- [ ] Update metadata to distinguish between "New Instance" and "Restored Instance" triggers. - -## Phase 9: Advanced Ecosystem Support -- [ ] **Reactive Chains**: Analyze WebFlux/Project Reactor lambdas (`doOnNext`, `flatMap`) for hidden triggers. -- [ ] **Interceptors & Filters**: Detect `HandlerInterceptor` and `Filter` implementations and map them to endpoints. - -## Phase 10: External Dependency Interop -- [x] **Library Hints**: Implementation complete. Supports `hints.json` in project root or `src/main/resources/`. - - **Format**: `[{"methodFqn": "class.method", "event": "EVENT"}]` - - **Purpose**: Maps external/compiled library calls to State Machine events when source code is unavailable. -- [ ] **External JAR Analysis**: (Optional) Configure JDT to scan source-attachments of dependencies if available. - -**Note on Library Hints**: This feature is an optional "Power User" escape hatch. The analyzer works automatically for all local source code; `hints.json` is only required to bridge the gap for third-party libraries. - 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"); + } }