Compare commits
2 Commits
1655f5285c
...
74a4e66a33
| Author | SHA1 | Date | |
|---|---|---|---|
| 74a4e66a33 | |||
| 4511e0e02f |
@@ -1,71 +1,26 @@
|
|||||||
# Execution Plan for Extended Analysis (Revised for Robustness)
|
|
||||||
|
|
||||||
## Phase 0: Surgical Refactoring (The Enrichment Hook)
|
## Phase 11: Formal Compiler & Static Analysis Alignment (Limitations & Missing Features)
|
||||||
- [x] Create `AnalysisResult` and `CodebaseMetadata` models.
|
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:
|
||||||
- [x] Update `ExportService.generateOutputs` to accept `AnalysisResult`.
|
|
||||||
- [x] Implement `EnrichmentService` and `AnalysisEnricher` interface.
|
|
||||||
- [x] Wrap current parser outputs in `AnalysisResult` within `ExportService`.
|
|
||||||
|
|
||||||
## Phase 1: Foundation & Semantic Core
|
1. **Full Pointer & Alias Analysis**:
|
||||||
- [ ] **JDT Binding Resolution**: Configure `ASTParser` with project classpath and `setResolveBindings(true)`.
|
- 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.
|
||||||
- [ ] **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.
|
|
||||||
|
|
||||||
## Phase 2: Trigger Discovery & Instance Identity
|
2. **Advanced Loop Fixpoints for Fields**:
|
||||||
- [ ] **GenericEventDetector**: Find `sendEvent` calls using `ASTVisitor`.
|
- 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.
|
||||||
- [ ] **InstanceIdentifier**: Detect which State Machine is being targeted (via `@Qualifier`, bean names, or generic types `StateMachine<S, E>`).
|
|
||||||
- [ ] **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.
|
|
||||||
|
|
||||||
## Phase 3: Entry Point Mapping
|
3. **Exception Path Analysis (Exception CFG)**:
|
||||||
- [ ] **SpringMvcDetector**: Detect REST endpoints (using `ConstantResolver` for paths).
|
- 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.
|
||||||
- [ ] **MessageListenerDetector**: Detect Kafka/JMS listeners (using `ConstantResolver` for topics/queues).
|
|
||||||
- [ ] **ConfigurableTriggerDetector**: Support user-defined patterns for custom frameworks.
|
|
||||||
|
|
||||||
## Phase 4: Call Graph Integration
|
4. **Collection & Array Tracking**:
|
||||||
- [x] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls.
|
- 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)`).
|
||||||
- [ ] Support inheritance in call graph resolution (using JDT bindings to resolve method overrides).
|
|
||||||
|
|
||||||
## Phase 4.5: Hierarchical Robustness (Inheritance Support)
|
5. **Field Shadowing and Inheritance Semantics**:
|
||||||
- [ ] **Hierarchical Annotation Lookup**: Update `SpringMvcDetector` to scan interfaces and superclasses for inherited mapping annotations.
|
- 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.
|
||||||
- [ ] **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.
|
6. **Static Initialization Blocks and Static Mutable State**:
|
||||||
- [ ] **Base Class Trigger Discovery**: Ensure triggers in abstract base classes are correctly attributed to their concrete subclasses.
|
- 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.
|
|
||||||
|
|
||||||
|
|||||||
@@ -629,6 +629,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String resolveArgument(Expression expr) {
|
protected String resolveArgument(Expression expr) {
|
||||||
|
if (expr == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
if (expr instanceof LambdaExpression le) {
|
if (expr instanceof LambdaExpression le) {
|
||||||
ASTNode body = le.getBody();
|
ASTNode body = le.getBody();
|
||||||
if (body instanceof Expression bodyExpr) {
|
if (body instanceof Expression bodyExpr) {
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
|
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
|
||||||
|
if (expr == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||||
org.eclipse.jdt.core.dom.Expression unwrappedBuilder = unwrapMessageBuilder(expr);
|
org.eclipse.jdt.core.dom.Expression unwrappedBuilder = unwrapMessageBuilder(expr);
|
||||||
if (unwrappedBuilder != expr) {
|
if (unwrappedBuilder != expr) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
|
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
|
||||||
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
||||||
|
private final Map<ASTNode, Map<IVariableBinding, Expression>> objectFieldBindings = new HashMap<>();
|
||||||
|
|
||||||
public JdtDataFlowModel(CodebaseContext context) {
|
public JdtDataFlowModel(CodebaseContext context) {
|
||||||
this.context = 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
|
// Handle CastExpression
|
||||||
if (expr instanceof CastExpression ce) {
|
if (expr instanceof CastExpression ce) {
|
||||||
List<Expression> resolved = getReachingDefinitions(ce.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
|
List<Expression> 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<ReturnStatement> returns = findReturnStatements(md.getBody());
|
List<ReturnStatement> returns = findReturnStatements(md.getBody());
|
||||||
if (!returns.isEmpty()) {
|
if (!returns.isEmpty()) {
|
||||||
for (ReturnStatement rs : returns) {
|
for (ReturnStatement rs : returns) {
|
||||||
if (rs.getExpression() != null) {
|
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<IVariableBinding, Expression> buildFieldBindingsFromCic(ClassInstanceCreation cic) {
|
private Map<IVariableBinding, Expression> buildFieldBindingsFromCic(ClassInstanceCreation cic) {
|
||||||
|
return buildFieldBindingsFromCic(cic, new HashMap<>(), new HashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<IVariableBinding, Expression> buildFieldBindingsFromCic(
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
Map<IVariableBinding, Expression> callerParamBindings,
|
||||||
|
Map<IVariableBinding, Expression> callerInstanceFieldBindings) {
|
||||||
Map<IVariableBinding, Expression> fieldBindings = new HashMap<>();
|
Map<IVariableBinding, Expression> fieldBindings = new HashMap<>();
|
||||||
IMethodBinding cb = cic.resolveConstructorBinding();
|
IMethodBinding cb = cic.resolveConstructorBinding();
|
||||||
if (cb == null) return fieldBindings;
|
if (cb == null) return fieldBindings;
|
||||||
|
|
||||||
evaluateConstructor(cb, cic.arguments(), new HashMap<>(), fieldBindings, 0);
|
List<Expression> resolvedArguments = new ArrayList<>();
|
||||||
|
for (Object argObj : cic.arguments()) {
|
||||||
|
Expression arg = (Expression) argObj;
|
||||||
|
List<Expression> 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;
|
return fieldBindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +471,26 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
if (concreteType != null) {
|
if (concreteType != null) {
|
||||||
MethodDeclaration md = findMethodDeclarationInType(concreteType, mb);
|
MethodDeclaration md = findMethodDeclarationInType(concreteType, mb);
|
||||||
if (md != null) {
|
if (md != null) {
|
||||||
Map<IVariableBinding, Expression> concreteFieldBindings = buildFieldBindingsFromCic(cic);
|
Map<IVariableBinding, Expression> 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<ASTNode> 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));
|
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<IVariableBinding, Expression> 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
|
// If no polymorphic subclass targets were found, fallback to the declared method itself
|
||||||
@@ -470,6 +551,218 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
return targets;
|
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<IVariableBinding, Expression> fieldBindings) {
|
||||||
|
if (md == null || md.getBody() == null) return;
|
||||||
|
|
||||||
|
Map<IVariableBinding, Expression> 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<IVariableBinding, Expression> fieldBindings,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Set<ASTNode> 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<Expression> 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<TargetMethod> 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<IVariableBinding, Expression> fieldBindings,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Set<ASTNode> 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<ControlFlowGraph.CfgNode> reachFromD = new HashSet<>();
|
||||||
|
Queue<ControlFlowGraph.CfgNode> 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<ControlFlowGraph.CfgNode> 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<ControlFlowGraph.CfgNode> pathNodes = new HashSet<>(reachFromD);
|
||||||
|
pathNodes.retainAll(reachToU);
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> orderedPathNodes = new ArrayList<>(pathNodes);
|
||||||
|
List<ControlFlowGraph.CfgNode> 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) {
|
private MethodDeclaration findMethodDeclarationInType(ITypeBinding typeBinding, IMethodBinding mb) {
|
||||||
if (typeBinding == null || mb == null) return null;
|
if (typeBinding == null || mb == null) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -512,5 +512,166 @@ class JdtDataFlowModelTest {
|
|||||||
String val = model.resolveValue(initEv, context);
|
String val = model.resolveValue(initEv, context);
|
||||||
assertThat(val).isEqualTo("PAY");
|
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<Expression> defs = model.getReachingDefinitions(initEv);
|
||||||
|
assertThat(defs).hasSize(2);
|
||||||
|
|
||||||
|
List<String> values = defs.stream()
|
||||||
|
.filter(e -> e instanceof StringLiteral)
|
||||||
|
.map(e -> ((StringLiteral) e).getLiteralValue())
|
||||||
|
.toList();
|
||||||
|
assertThat(values).containsExactlyInAnyOrder("PAY", "CANCEL");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user