Compare commits
47 Commits
working_bu
...
382a221a62
| Author | SHA1 | Date | |
|---|---|---|---|
| 382a221a62 | |||
| f4188f4384 | |||
| 96fc975170 | |||
| 8d3fb0d90c | |||
| fc9d6f1f1c | |||
| 858e4524c8 | |||
| 93688ef59b | |||
| ff2c8f8cfb | |||
| 901b43195a | |||
| ed411f22d8 | |||
| c9d41de13f | |||
| df2a596e26 | |||
| a75b7acfe4 | |||
| 74a4e66a33 | |||
| 4511e0e02f | |||
| a7c3b08164 | |||
| 8d66af6d24 | |||
| 642f6f8926 | |||
| 76bb88e065 | |||
| 5c7cf3639b | |||
| 016f0e9b21 | |||
| 22a0d3f5aa | |||
| 7008f162b5 | |||
| 60c666f7a1 | |||
| 8a56e92a65 | |||
| e8543ace38 | |||
| 101f2b5dc9 | |||
| f525f3d3ce | |||
| 5ae233eb75 | |||
| 0bda5a0aeb | |||
| c35c75efa3 | |||
| bd55a66985 | |||
| 0ac70bd3c6 | |||
| b45513c5fb | |||
| 75fd45d983 | |||
| b35effd05c | |||
| f30538e8c9 | |||
| 131ccbeda0 | |||
| 14b311be2b | |||
| 1b690582d6 | |||
| 3ca834fa71 | |||
| c6b4a4be1e | |||
| 24a9be3a4e | |||
| 07d241a060 | |||
| 32de0a51c6 | |||
| cbb00e8a0f | |||
| 6596303b7d |
@@ -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.
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
.targetState(smSourceRaw)
|
.targetState(smSourceRaw)
|
||||||
.event(smEventRaw)
|
.event(smEventRaw)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)) {
|
||||||
matched.add(mt);
|
matched.add(mt);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -70,7 +70,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
.targetState(targetRaw)
|
.targetState(targetRaw)
|
||||||
.event(smEventRaw)
|
.event(smEventRaw)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)) {
|
||||||
matched.add(mt);
|
matched.add(mt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,8 +100,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||||
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName);
|
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String simplify(String name) {
|
private String simplify(String name) {
|
||||||
|
|||||||
@@ -23,11 +23,24 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
|||||||
boolean hasPolyMatch = false;
|
boolean hasPolyMatch = false;
|
||||||
for (String pe : polyEvents) {
|
for (String pe : polyEvents) {
|
||||||
if (pe.contains(".") && smEventRaw.contains(".")) {
|
if (pe.contains(".") && smEventRaw.contains(".")) {
|
||||||
if (smEventRaw.equals(pe) || smEventRaw.endsWith("." + pe)) {
|
if (smEventRaw.equals(pe) || smEventRaw.endsWith("." + pe) || pe.endsWith("." + smEventRaw)) {
|
||||||
hasPolyMatch = true;
|
hasPolyMatch = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continue; // Stricter matching: do not fallback if both have qualifiers but don't match
|
String classPe = pe.substring(0, pe.lastIndexOf('.'));
|
||||||
|
String classSm = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
||||||
|
if (classPe.contains(".") && classSm.contains(".")) {
|
||||||
|
String p1 = classPe.substring(0, classPe.lastIndexOf('.'));
|
||||||
|
String p2 = classSm.substring(0, classSm.lastIndexOf('.'));
|
||||||
|
if (!p1.equals(p2) && !p1.startsWith(p2 + ".") && !p2.startsWith(p1 + ".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String simpleClassPe = classPe.contains(".") ? classPe.substring(classPe.lastIndexOf('.') + 1) : classPe;
|
||||||
|
String simpleClassSm = classSm.contains(".") ? classSm.substring(classSm.lastIndexOf('.') + 1) : classSm;
|
||||||
|
if (!simpleClassPe.equals(simpleClassSm)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String simplePe = pe;
|
String simplePe = pe;
|
||||||
@@ -37,7 +50,8 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
|||||||
String simplifiedPe = simplify(simplePe);
|
String simplifiedPe = simplify(simplePe);
|
||||||
|
|
||||||
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
||||||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) {
|
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent) ||
|
||||||
|
isGuardedContains(smEvent, simplifiedPe)) {
|
||||||
hasPolyMatch = true;
|
hasPolyMatch = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -47,14 +61,58 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
|||||||
if (polyEvents.isEmpty() && isWildcard) return true;
|
if (polyEvents.isEmpty() && isWildcard) return true;
|
||||||
|
|
||||||
if (rawTriggerEvent.contains(".") && smEventRaw.contains(".")) {
|
if (rawTriggerEvent.contains(".") && smEventRaw.contains(".")) {
|
||||||
if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent)) {
|
if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent) || rawTriggerEvent.endsWith("." + smEventRaw)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
String classTrig = rawTriggerEvent.substring(0, rawTriggerEvent.lastIndexOf('.'));
|
||||||
|
String classSm = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
||||||
|
if (classTrig.contains(".") && classSm.contains(".")) {
|
||||||
|
String p1 = classTrig.substring(0, classTrig.lastIndexOf('.'));
|
||||||
|
String p2 = classSm.substring(0, classSm.lastIndexOf('.'));
|
||||||
|
if (!p1.equals(p2) && !p1.startsWith(p2 + ".") && !p2.startsWith(p1 + ".")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String simpleClassTrig = classTrig.contains(".") ? classTrig.substring(classTrig.lastIndexOf('.') + 1) : classTrig;
|
||||||
|
String simpleClassSm = classSm.contains(".") ? classSm.substring(classSm.lastIndexOf('.') + 1) : classSm;
|
||||||
|
if (!simpleClassTrig.equals(simpleClassSm)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String triggerEvent = simplify(rawTriggerEvent);
|
String triggerEvent = simplify(rawTriggerEvent);
|
||||||
return smEvent.equals(triggerEvent);
|
if (smEvent.equalsIgnoreCase(triggerEvent)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isGuardedContains(smEvent, triggerEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||||
|
if (smEvent == null || triggerEvent == null) return false;
|
||||||
|
// Only fire if it looks like a simple identifier (no spaces, no parens)
|
||||||
|
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
|
||||||
|
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
|
||||||
|
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String smLower = smEvent.toLowerCase();
|
||||||
|
String triggerLower = triggerEvent.toLowerCase();
|
||||||
|
|
||||||
|
if (smLower.equals(triggerLower)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match if one contains the other as a whole word/segment.
|
||||||
|
// We quote the patterns to avoid regex syntax injection issues.
|
||||||
|
String pattern1 = "\\b" + java.util.regex.Pattern.quote(smLower) + "\\b";
|
||||||
|
String pattern2 = "\\b" + java.util.regex.Pattern.quote(triggerLower) + "\\b";
|
||||||
|
|
||||||
|
// Treat underscores and hyphens as word boundary separators
|
||||||
|
String smNormalized = smLower.replace('_', ' ').replace('-', ' ');
|
||||||
|
String triggerNormalized = triggerLower.replace('_', ' ').replace('-', ' ');
|
||||||
|
|
||||||
|
return smNormalized.matches(".*" + pattern2 + ".*") || triggerNormalized.matches(".*" + pattern1 + ".*");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWildcardVariable(String eventStr) {
|
private boolean isWildcardVariable(String eventStr) {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ForwardPathEstimationEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
|
private final boolean isEnabled;
|
||||||
|
private final PathValueEstimator estimator;
|
||||||
|
|
||||||
|
public ForwardPathEstimationEnricher() {
|
||||||
|
this.isEnabled = Boolean.parseBoolean(System.getProperty("analyzer.forward-path-estimation.enabled", "false"));
|
||||||
|
this.estimator = new SymbolicPathValueEstimator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
|
if (!isEnabled || result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Running Symbolic Forward Path Analysis for {}", result.getName());
|
||||||
|
|
||||||
|
List<CallChain> chains = result.getMetadata().getCallChains();
|
||||||
|
List<CallChain> updatedChains = chains.stream().map(chain -> {
|
||||||
|
if (chain.getTriggerPoint() == null || chain.getTriggerPoint().getEvent() == null) return chain;
|
||||||
|
|
||||||
|
// If the event looks like a dynamic variable (no dots, not an enum format)
|
||||||
|
if (!chain.getTriggerPoint().getEvent().contains(".")) {
|
||||||
|
List<String> estimatedValues = estimator.estimatePossibleValues(chain, context);
|
||||||
|
if (estimatedValues != null && !estimatedValues.isEmpty()) {
|
||||||
|
log.info("Symbolic Path Analysis estimated values for {} -> {}", chain.getTriggerPoint().getEvent(), estimatedValues);
|
||||||
|
|
||||||
|
List<String> polyEvents = new ArrayList<>();
|
||||||
|
if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
|
||||||
|
polyEvents.addAll(chain.getTriggerPoint().getPolymorphicEvents());
|
||||||
|
}
|
||||||
|
polyEvents.addAll(estimatedValues);
|
||||||
|
|
||||||
|
TriggerPoint newTp = chain.getTriggerPoint().toBuilder()
|
||||||
|
.polymorphicEvents(polyEvents)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return chain.toBuilder().triggerPoint(newTp).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata updatedMetadata =
|
||||||
|
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||||
|
.triggers(result.getMetadata().getTriggers())
|
||||||
|
.entryPoints(result.getMetadata().getEntryPoints())
|
||||||
|
.callChains(updatedChains)
|
||||||
|
.properties(result.getMetadata().getProperties())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
result.setMetadata(updatedMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PathValueEstimator {
|
||||||
|
List<String> estimatePossibleValues(CallChain chain, CodebaseContext context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class SymbolicPathValueEstimator implements PathValueEstimator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> estimatePossibleValues(CallChain chain, CodebaseContext context) {
|
||||||
|
if (chain.getMethodChain() == null || chain.getMethodChain().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> collectedConstants = new HashSet<>();
|
||||||
|
|
||||||
|
for (String methodFqn : chain.getMethodChain()) {
|
||||||
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
|
if (lastDot == -1) continue;
|
||||||
|
|
||||||
|
String className = methodFqn.substring(0, lastDot);
|
||||||
|
String methodName = methodFqn.substring(lastDot + 1);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) continue;
|
||||||
|
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md == null || md.getBody() == null) continue;
|
||||||
|
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if ((node.getName().getIdentifier().equals("equals") || node.getName().getIdentifier().equals("equalsIgnoreCase"))
|
||||||
|
&& node.arguments().size() == 1) {
|
||||||
|
|
||||||
|
Expression caller = node.getExpression();
|
||||||
|
Expression arg = (Expression) node.arguments().get(0);
|
||||||
|
|
||||||
|
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||||
|
|
||||||
|
String callerVal = resolver.resolve(caller, context);
|
||||||
|
if (callerVal != null && !callerVal.isEmpty()) {
|
||||||
|
addValue(collectedConstants, callerVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
String argVal = resolver.resolve(arg, context);
|
||||||
|
if (argVal != null && !argVal.isEmpty()) {
|
||||||
|
addValue(collectedConstants, argVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(InfixExpression node) {
|
||||||
|
if (node.getOperator() == InfixExpression.Operator.EQUALS) {
|
||||||
|
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||||
|
String left = resolver.resolve(node.getLeftOperand(), context);
|
||||||
|
if (left != null && !left.isEmpty()) {
|
||||||
|
addValue(collectedConstants, left);
|
||||||
|
}
|
||||||
|
String right = resolver.resolve(node.getRightOperand(), context);
|
||||||
|
if (right != null && !right.isEmpty()) {
|
||||||
|
addValue(collectedConstants, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SwitchStatement node) {
|
||||||
|
for (Object stmt : node.statements()) {
|
||||||
|
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
|
||||||
|
for (Object expr : sc.expressions()) {
|
||||||
|
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||||
|
String val = resolver.resolve((Expression) expr, context);
|
||||||
|
if (val != null && !val.isEmpty()) {
|
||||||
|
addValue(collectedConstants, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SwitchExpression node) {
|
||||||
|
for (Object stmt : node.statements()) {
|
||||||
|
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
|
||||||
|
for (Object expr : sc.expressions()) {
|
||||||
|
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||||
|
String val = resolver.resolve((Expression) expr, context);
|
||||||
|
if (val != null && !val.isEmpty()) {
|
||||||
|
addValue(collectedConstants, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectedConstants.isEmpty()) return null;
|
||||||
|
return new ArrayList<>(collectedConstants);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addValue(Set<String> collectedConstants, String val) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
String parsed = parseEnumSetElement(eVal);
|
||||||
|
if (!collectedConstants.contains(parsed)) collectedConstants.add(parsed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collectedConstants.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseEnumSetElement(String element) {
|
||||||
|
if (element.contains(".")) {
|
||||||
|
return element.substring(element.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
public interface BeanResolutionEngine {
|
public interface BeanResolutionEngine {
|
||||||
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName);
|
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -8,7 +9,35 @@ import java.util.Set;
|
|||||||
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||||
|
// Precise FQN Type argument match
|
||||||
|
if (chain.getTriggerPoint() != null && context != null && currentMachineName != null) {
|
||||||
|
String triggerEventFqn = chain.getTriggerPoint().getEventTypeFqn();
|
||||||
|
String triggerStateFqn = chain.getTriggerPoint().getStateTypeFqn();
|
||||||
|
if (triggerEventFqn != null || triggerStateFqn != null) {
|
||||||
|
String[] machineTypes = getStateMachineTypeArguments(currentMachineName, context);
|
||||||
|
String machineStateFqn = machineTypes[0];
|
||||||
|
String machineEventFqn = machineTypes[1];
|
||||||
|
|
||||||
|
if (triggerEventFqn != null && machineEventFqn != null) {
|
||||||
|
if (eraseGenerics(triggerEventFqn).equals(eraseGenerics(machineEventFqn))) {
|
||||||
|
return true; // Match!
|
||||||
|
}
|
||||||
|
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
||||||
|
return false; // Mismatch!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (triggerStateFqn != null && machineStateFqn != null) {
|
||||||
|
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
|
||||||
|
return true; // Match!
|
||||||
|
}
|
||||||
|
if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
||||||
|
return false; // Mismatch!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String targetVar = chain.getContextMachineId();
|
String targetVar = chain.getContextMachineId();
|
||||||
if (targetVar == null && chain.getTriggerPoint() != null) {
|
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||||
targetVar = chain.getTriggerPoint().getStateMachineId();
|
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||||
@@ -40,6 +69,21 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
boolean hasPositiveMatch = false;
|
boolean hasPositiveMatch = false;
|
||||||
boolean hasStrongMismatch = false;
|
boolean hasStrongMismatch = false;
|
||||||
|
|
||||||
|
if (chain.getTriggerPoint() != null && chain.getTriggerPoint().getClassName() != null) {
|
||||||
|
String chainClass = chain.getTriggerPoint().getClassName();
|
||||||
|
String chainPackage = getPackageName(chainClass);
|
||||||
|
String chainSimple = getSimpleClassName(chainClass);
|
||||||
|
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
||||||
|
|
||||||
|
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
|
||||||
|
hasPositiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
|
||||||
|
hasPositiveMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (String method : chain.getMethodChain()) {
|
for (String method : chain.getMethodChain()) {
|
||||||
String chainClass = getClassNameOnly(method);
|
String chainClass = getClassNameOnly(method);
|
||||||
String chainPackage = getPackageName(chainClass);
|
String chainPackage = getPackageName(chainClass);
|
||||||
@@ -54,7 +98,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
hasPositiveMatch = true;
|
hasPositiveMatch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
|
if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix, smSimple, chainSimple)) {
|
||||||
hasStrongMismatch = true;
|
hasStrongMismatch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,37 +146,68 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix, String smSimple, String chainSimple) {
|
||||||
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
||||||
|
if (smPrefix == null || chainPrefix == null) return false;
|
||||||
|
|
||||||
String[] p1 = smPackage.split("\\.");
|
String smLower = smPrefix.toLowerCase();
|
||||||
String[] p2 = chainPackage.split("\\.");
|
String chainLower = chainPrefix.toLowerCase();
|
||||||
|
|
||||||
int matchIdx = -1;
|
// Ignore generic/technical class prefixes
|
||||||
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
if (smLower.equals("state") || smLower.equals("statemachine") || smLower.equals("config") || smLower.equals("configuration") || smLower.equals("adapter") ||
|
||||||
if (p1[i].equals(p2[i])) {
|
smLower.equals("enterprise") || smLower.equals("app") || smLower.equals("global") || smLower.equals("core") || smLower.equals("project") || smLower.equals("main") || smLower.equals("base") || smLower.equals("common") || smLower.equals("shared") ||
|
||||||
matchIdx = i;
|
chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config") || chainLower.equals("configuration") || chainLower.equals("adapter") ||
|
||||||
} else {
|
chainLower.equals("enterprise") || chainLower.equals("app") || chainLower.equals("global") || chainLower.equals("core") || chainLower.equals("project") || chainLower.equals("main") || chainLower.equals("base") || chainLower.equals("common") || chainLower.equals("shared")) {
|
||||||
break;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If class prefixes diverge, check if they are from different domains
|
||||||
|
if (!smLower.equals(chainLower)) {
|
||||||
|
// If one prefix is present as a domain/feature segment in the other package, they are related
|
||||||
|
if (chainPackage.toLowerCase().contains(smLower) || smPackage.toLowerCase().contains(chainLower)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> smDivergentTerms = new HashSet<>();
|
// Otherwise, if they diverge under a shared project package root, it is a mismatch
|
||||||
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase());
|
String[] p1 = smPackage.split("\\.");
|
||||||
for (int i = matchIdx + 1; i < p1.length; i++) {
|
String[] p2 = chainPackage.split("\\.");
|
||||||
smDivergentTerms.add(p1[i].toLowerCase());
|
int matchIdx = -1;
|
||||||
}
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
|
if (p1[i].equals(p2[i])) {
|
||||||
|
matchIdx = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchIdx >= 1) {
|
||||||
|
// If they diverge under a shared root, check if the divergence is only on generic package layers
|
||||||
|
boolean onlyGenericDivergence = true;
|
||||||
|
Set<String> genericLayers = Set.of(
|
||||||
|
"config", "configuration", "service", "services", "impl", "api", "web",
|
||||||
|
"controller", "controllers", "messaging", "listener", "listeners",
|
||||||
|
"repository", "repositories", "db", "model", "models", "domain",
|
||||||
|
"constants", "utils", "helper", "helpers", "event", "events",
|
||||||
|
"state", "states", "transition", "transitions"
|
||||||
|
);
|
||||||
|
for (int i = matchIdx + 1; i < p1.length; i++) {
|
||||||
|
if (!genericLayers.contains(p1[i].toLowerCase())) {
|
||||||
|
onlyGenericDivergence = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (onlyGenericDivergence) {
|
||||||
|
for (int i = matchIdx + 1; i < p2.length; i++) {
|
||||||
|
if (!genericLayers.contains(p2[i].toLowerCase())) {
|
||||||
|
onlyGenericDivergence = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> chainDivergentTerms = new HashSet<>();
|
if (!onlyGenericDivergence) {
|
||||||
if (chainPrefix != null) chainDivergentTerms.add(chainPrefix.toLowerCase());
|
return true; // Mismatch under a shared root
|
||||||
for (int i = matchIdx + 1; i < p2.length; i++) {
|
}
|
||||||
chainDivergentTerms.add(p2[i].toLowerCase());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!smDivergentTerms.isEmpty() && !chainDivergentTerms.isEmpty()) {
|
|
||||||
Set<String> intersection = new HashSet<>(smDivergentTerms);
|
|
||||||
intersection.retainAll(chainDivergentTerms);
|
|
||||||
return intersection.isEmpty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -185,4 +260,120 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String[] getStateMachineTypeArguments(String machineName, CodebaseContext context) {
|
||||||
|
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(machineName);
|
||||||
|
if (td != null) {
|
||||||
|
org.eclipse.jdt.core.dom.Type superclass = td.getSuperclassType();
|
||||||
|
Set<String> visited = new HashSet<>();
|
||||||
|
visited.add(machineName);
|
||||||
|
return findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||||
|
}
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] findTypeArgumentsRecursively(org.eclipse.jdt.core.dom.Type type, CodebaseContext context, org.eclipse.jdt.core.dom.CompilationUnit cu, Set<String> visited) {
|
||||||
|
if (type == null) return new String[]{null, null};
|
||||||
|
|
||||||
|
if (type instanceof org.eclipse.jdt.core.dom.ParameterizedType pt) {
|
||||||
|
String rawTypeName = pt.getType().toString();
|
||||||
|
if (rawTypeName.equals("StateMachineConfigurerAdapter") || rawTypeName.equals("EnumStateMachineConfigurerAdapter") ||
|
||||||
|
rawTypeName.endsWith(".StateMachineConfigurerAdapter") || rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter")) {
|
||||||
|
java.util.List<?> typeArgs = pt.typeArguments();
|
||||||
|
if (typeArgs.size() >= 2) {
|
||||||
|
String stateType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(0), cu, context);
|
||||||
|
String eventType = resolveTypeToFqn((org.eclipse.jdt.core.dom.Type) typeArgs.get(1), cu, context);
|
||||||
|
return new String[]{stateType, eventType};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into the raw type declaration's superclass
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (visited.add(fqn)) {
|
||||||
|
org.eclipse.jdt.core.dom.Type superclass = (td instanceof org.eclipse.jdt.core.dom.TypeDeclaration) ? ((org.eclipse.jdt.core.dom.TypeDeclaration) td).getSuperclassType() : null;
|
||||||
|
String[] res = findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||||
|
if (res[0] != null || res[1] != null) return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a simple type (e.g. MyBaseConfig)
|
||||||
|
String simpleName = type.toString();
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (visited.add(fqn)) {
|
||||||
|
org.eclipse.jdt.core.dom.Type superclass = (td instanceof org.eclipse.jdt.core.dom.TypeDeclaration) ? ((org.eclipse.jdt.core.dom.TypeDeclaration) td).getSuperclassType() : null;
|
||||||
|
String[] res = findTypeArgumentsRecursively(superclass, context, (org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot(), visited);
|
||||||
|
if (res[0] != null || res[1] != null) return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String eraseGenerics(String type) {
|
||||||
|
if (type == null) return null;
|
||||||
|
int idx = type.indexOf('<');
|
||||||
|
if (idx != -1) {
|
||||||
|
return type.substring(0, idx);
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTypeToFqn(org.eclipse.jdt.core.dom.Type type, org.eclipse.jdt.core.dom.CompilationUnit cu, CodebaseContext context) {
|
||||||
|
if (type == null) return null;
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((org.eclipse.jdt.core.dom.SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((org.eclipse.jdt.core.dom.ParameterizedType) type).getType(), cu, context);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simpleName.contains("<")) {
|
||||||
|
simpleName = simpleName.substring(0, simpleName.indexOf('<'));
|
||||||
|
}
|
||||||
|
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
org.eclipse.jdt.core.dom.ImportDeclaration imp = (org.eclipse.jdt.core.dom.ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
|
if (!packageName.isEmpty()) {
|
||||||
|
org.eclipse.jdt.core.dom.AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||||
|
if (localTd != null) return context.getFqn(localTd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTypeMismatched(String type1, String type2) {
|
||||||
|
if (type1 == null || type2 == null) return false;
|
||||||
|
type1 = eraseGenerics(type1);
|
||||||
|
type2 = eraseGenerics(type2);
|
||||||
|
if (type1.equals("java.lang.String") || type1.equals("String") || type1.equals("java.lang.Object") || type1.equals("Object")) return false;
|
||||||
|
if (type2.equals("java.lang.String") || type2.equals("String") || type2.equals("java.lang.Object") || type2.equals("Object")) return false;
|
||||||
|
return !type1.equals(type2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSingleStateMachine(CodebaseContext context) {
|
||||||
|
if (context == null) return false;
|
||||||
|
int count = 0;
|
||||||
|
count += context.findEntryPointClasses(java.util.List.of("EnableStateMachineFactory", "EnableStateMachine")).size();
|
||||||
|
count += context.findBeanMethodsReturning(java.util.Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory")).size();
|
||||||
|
return count <= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class CallEdge {
|
public class CallEdge {
|
||||||
private final String targetMethod;
|
private String targetMethod;
|
||||||
private final List<String> arguments;
|
private List<String> arguments;
|
||||||
|
private String receiver;
|
||||||
|
|
||||||
|
public CallEdge(String targetMethod, List<String> arguments) {
|
||||||
|
this(targetMethod, arguments, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder(toBuilder = true)
|
||||||
@Jacksonized
|
@Jacksonized
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class TriggerPoint {
|
public class TriggerPoint {
|
||||||
@@ -20,5 +20,9 @@ public class TriggerPoint {
|
|||||||
private final String stateMachineId; // Optional: to link to a specific SM instance
|
private final String stateMachineId; // Optional: to link to a specific SM instance
|
||||||
private final String sourceState; // Optional: if we can determine the expected current state
|
private final String sourceState; // Optional: if we can determine the expected current state
|
||||||
private final int lineNumber;
|
private final int lineNumber;
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
private final String stateTypeFqn; // Type of State (e.g. OrderStates FQN)
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
private final String eventTypeFqn; // Type of Event (e.g. OrderEvents FQN)
|
||||||
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
|
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ public class ConstantResolver {
|
|||||||
return resolveInternal(expr, context, new HashSet<>());
|
return resolveInternal(expr, context, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context) {
|
||||||
|
return evaluateSwitchExpression(se, paramValues, context, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context) {
|
||||||
|
return evaluateSwitchStatement(ss, paramValues, context, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) {
|
private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) {
|
||||||
if (expr == null) return null;
|
if (expr == null) return null;
|
||||||
|
|
||||||
@@ -64,6 +72,9 @@ public class ConstantResolver {
|
|||||||
String val = resolveManual(qn, context, visited);
|
String val = resolveManual(qn, context, visited);
|
||||||
return val != null ? val : qn.toString();
|
return val != null ? val : qn.toString();
|
||||||
}
|
}
|
||||||
|
if (expr instanceof FieldAccess fa) {
|
||||||
|
return resolveManual(fa.getName(), context, visited);
|
||||||
|
}
|
||||||
if (expr instanceof SimpleName sn) {
|
if (expr instanceof SimpleName sn) {
|
||||||
return resolveManual(sn, context, visited);
|
return resolveManual(sn, context, visited);
|
||||||
}
|
}
|
||||||
@@ -82,9 +93,16 @@ public class ConstantResolver {
|
|||||||
|
|
||||||
// Fallback for unresolved AST nodes
|
// Fallback for unresolved AST nodes
|
||||||
if (mi.getExpression() instanceof QualifiedName qn) {
|
if (mi.getExpression() instanceof QualifiedName qn) {
|
||||||
return qn.getName().getIdentifier();
|
System.out.println("RETURNING qn name: " + qn.getName().getIdentifier()); return qn.getName().getIdentifier();
|
||||||
} else if (mi.getExpression() instanceof SimpleName sn) {
|
} else if (mi.getExpression() instanceof SimpleName sn) {
|
||||||
return sn.getIdentifier();
|
System.out.println("RETURNING sn name: " + sn.getIdentifier()); return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("valueOf".equals(mi.getName().getIdentifier()) && mi.getExpression() != null) {
|
||||||
|
java.util.List<String> enumValues = context.getEnumValues(mi.getExpression().toString());
|
||||||
|
if (enumValues != null && !enumValues.isEmpty()) {
|
||||||
|
return "ENUM_SET:" + String.join(",", enumValues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,13 +116,43 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
TypeDeclaration td = findEnclosingType(mi);
|
TypeDeclaration td = null;
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
ITypeBinding typeBinding = mi.getExpression().resolveTypeBinding();
|
||||||
|
if (typeBinding != null) {
|
||||||
|
String typeName = typeBinding.getQualifiedName();
|
||||||
|
if (typeName != null && !typeName.isEmpty()) {
|
||||||
|
td = context.getTypeDeclaration(typeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td == null && mi.getExpression() instanceof SimpleName sn) {
|
||||||
|
String typeName = resolveLocalType(sn);
|
||||||
|
if (typeName != null) {
|
||||||
|
td = context.getTypeDeclaration(typeName);
|
||||||
|
if (td == null) {
|
||||||
|
CompilationUnit cu = (CompilationUnit) mi.getRoot();
|
||||||
|
td = context.getTypeDeclaration(typeName, cu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td == null) {
|
||||||
|
td = findEnclosingType(mi);
|
||||||
|
}
|
||||||
|
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||||
if (md != null && md.getReturnType2() != null) {
|
if (md != null) {
|
||||||
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
|
String evaluated = evaluateMethodOutput(mi, md, context, visited);
|
||||||
if (values != null && !values.isEmpty()) {
|
if (evaluated != null) return evaluated;
|
||||||
return "ENUM_SET:" + String.join(",", values);
|
|
||||||
|
if (md.getReturnType2() != null) {
|
||||||
|
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
return "ENUM_SET:" + String.join(",", values);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,6 +162,275 @@ public class ConstantResolver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String evaluateMethodOutput(MethodInvocation mi, MethodDeclaration md, CodebaseContext context, Set<String> visited) {
|
||||||
|
if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null;
|
||||||
|
|
||||||
|
TypeDeclaration td = findEnclosingType(md);
|
||||||
|
if (td == null) return null;
|
||||||
|
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||||
|
|
||||||
|
if (!visited.add(methodFqn)) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Cycle detected in method evaluation for: {} (visited={})", methodFqn, visited);
|
||||||
|
}
|
||||||
|
return null; // Cycle detected in method evaluation
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.util.Map<String, String> localVars = new java.util.HashMap<>();
|
||||||
|
java.util.Set<String> declaredLocals = new java.util.HashSet<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < mi.arguments().size(); i++) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(i);
|
||||||
|
String val = resolveInternal(arg, context, visited);
|
||||||
|
if (val != null) {
|
||||||
|
org.eclipse.jdt.core.dom.SingleVariableDeclaration param =
|
||||||
|
(org.eclipse.jdt.core.dom.SingleVariableDeclaration) md.parameters().get(i);
|
||||||
|
String paramName = param.getName().getIdentifier();
|
||||||
|
localVars.put(paramName, val);
|
||||||
|
declaredLocals.add(paramName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited);
|
||||||
|
} finally {
|
||||||
|
visited.remove(methodFqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates a method body with pre-supplied local values (e.g. field values derived
|
||||||
|
* from the specific {@code ClassInstanceCreation} that created the receiver object).
|
||||||
|
* Used by {@link click.kamil.springstatemachineexporter.analysis.service.AbstractCallGraphEngine}
|
||||||
|
* to resolve transforming getters — methods that map a constructor-injected enum to a
|
||||||
|
* different state-machine enum via switch expressions.
|
||||||
|
*/
|
||||||
|
public String evaluateMethodBodyWithLocals(MethodDeclaration md,
|
||||||
|
java.util.Map<String, String> prebuiltLocals,
|
||||||
|
CodebaseContext context,
|
||||||
|
Set<String> visited) {
|
||||||
|
if (md.getBody() == null) return null;
|
||||||
|
|
||||||
|
TypeDeclaration td = findEnclosingType(md);
|
||||||
|
if (td == null) return null;
|
||||||
|
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||||
|
|
||||||
|
if (!visited.add(methodFqn)) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Cycle detected in method body evaluation for: {} (visited={})", methodFqn, visited);
|
||||||
|
}
|
||||||
|
return null; // Cycle detected in method evaluation
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.util.Map<String, String> localVars = new java.util.HashMap<>(prebuiltLocals);
|
||||||
|
java.util.Set<String> declaredLocals = new java.util.HashSet<>(prebuiltLocals.keySet());
|
||||||
|
return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited);
|
||||||
|
} finally {
|
||||||
|
visited.remove(methodFqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core body-evaluation logic shared by {@link #evaluateMethodOutput} and
|
||||||
|
* {@link #evaluateMethodBodyWithLocals}. Visits the block, tracks assignments,
|
||||||
|
* evaluates switch statements/expressions, and captures the first resolved return value.
|
||||||
|
*/
|
||||||
|
private String evaluateBody(org.eclipse.jdt.core.dom.Block body,
|
||||||
|
java.util.Map<String, String> localVars,
|
||||||
|
java.util.Set<String> declaredLocals,
|
||||||
|
CodebaseContext context,
|
||||||
|
Set<String> visited) {
|
||||||
|
String[] finalResult = new String[1];
|
||||||
|
|
||||||
|
body.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragmentObj : vds.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
String varName = fragment.getName().getIdentifier();
|
||||||
|
declaredLocals.add(varName);
|
||||||
|
if (fragment.getInitializer() != null) {
|
||||||
|
String rhsVal = resolveExpressionWithParams(fragment.getInitializer(), localVars);
|
||||||
|
if (rhsVal == null) rhsVal = resolveInternal(fragment.getInitializer(), context, visited);
|
||||||
|
if (rhsVal != null) localVars.put(varName, rhsVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(vds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment assignment) {
|
||||||
|
Expression lhs = assignment.getLeftHandSide();
|
||||||
|
String varName = null;
|
||||||
|
if (lhs instanceof SimpleName sn) {
|
||||||
|
varName = sn.getIdentifier();
|
||||||
|
} else if (lhs instanceof FieldAccess fa) {
|
||||||
|
varName = "this." + fa.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars);
|
||||||
|
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||||
|
if (varName != null && rhsVal != null) {
|
||||||
|
if (varName.startsWith("this.")) {
|
||||||
|
localVars.put(varName, rhsVal);
|
||||||
|
String bareName = varName.substring(5);
|
||||||
|
if (!declaredLocals.contains(bareName)) localVars.put(bareName, rhsVal);
|
||||||
|
} else {
|
||||||
|
localVars.put(varName, rhsVal);
|
||||||
|
if (!declaredLocals.contains(varName)) localVars.put("this." + varName, rhsVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(assignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(org.eclipse.jdt.core.dom.SwitchStatement ss) {
|
||||||
|
if (finalResult[0] == null) {
|
||||||
|
String result = evaluateSwitchStatement(ss, localVars, context, visited);
|
||||||
|
if (result != null) finalResult[0] = result;
|
||||||
|
}
|
||||||
|
return false; // children handled inside evaluateSwitchStatement
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||||
|
if (finalResult[0] == null) {
|
||||||
|
if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
|
||||||
|
String result = evaluateSwitchExpression(se, localVars, context, visited);
|
||||||
|
if (result != null) finalResult[0] = result;
|
||||||
|
} else if (rs.getExpression() != null) {
|
||||||
|
String result = resolveExpressionWithParams(rs.getExpression(), localVars);
|
||||||
|
if (result == null) result = resolveInternal(rs.getExpression(), context, visited);
|
||||||
|
if (result != null) finalResult[0] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(rs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return finalResult[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
|
||||||
|
String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues);
|
||||||
|
if (switchVar == null) return null;
|
||||||
|
|
||||||
|
String switchType = null;
|
||||||
|
if (switchVar.contains(".")) {
|
||||||
|
switchType = switchVar.substring(0, switchVar.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchingCase = false;
|
||||||
|
for (Object stmtObj : ss.statements()) {
|
||||||
|
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
|
||||||
|
matchingCase = false;
|
||||||
|
if (sc.isDefault()) {
|
||||||
|
matchingCase = true;
|
||||||
|
} else {
|
||||||
|
for (Object exprObj : sc.expressions()) {
|
||||||
|
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
|
||||||
|
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
|
||||||
|
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
|
||||||
|
caseVal = caseSn.getIdentifier();
|
||||||
|
if (switchType != null) {
|
||||||
|
caseVal = switchType + "." + caseVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (caseVal != null) {
|
||||||
|
if (switchVar.contains(".") && caseVal.contains(".")) {
|
||||||
|
if (switchVar.equals(caseVal)) {
|
||||||
|
matchingCase = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (switchVar.endsWith(caseVal)) {
|
||||||
|
matchingCase = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (matchingCase) {
|
||||||
|
if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||||
|
return resolveInternal(rs.getExpression(), context, visited);
|
||||||
|
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) {
|
||||||
|
return resolveInternal(ys.getExpression(), context, visited);
|
||||||
|
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
||||||
|
return resolveInternal(es.getExpression(), context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String evaluateSwitchExpression(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
|
||||||
|
String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues);
|
||||||
|
if (switchVar == null) return null;
|
||||||
|
|
||||||
|
String switchType = null;
|
||||||
|
if (switchVar.contains(".")) {
|
||||||
|
switchType = switchVar.substring(0, switchVar.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchingCase = false;
|
||||||
|
for (Object stmtObj : se.statements()) {
|
||||||
|
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
|
||||||
|
matchingCase = false;
|
||||||
|
if (sc.isDefault()) {
|
||||||
|
matchingCase = true;
|
||||||
|
} else {
|
||||||
|
for (Object exprObj : sc.expressions()) {
|
||||||
|
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
|
||||||
|
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
|
||||||
|
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
|
||||||
|
caseVal = caseSn.getIdentifier();
|
||||||
|
if (switchType != null) {
|
||||||
|
caseVal = switchType + "." + caseVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (caseVal != null) {
|
||||||
|
if (switchVar.contains(".") && caseVal.contains(".")) {
|
||||||
|
if (switchVar.equals(caseVal)) {
|
||||||
|
matchingCase = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (switchVar.endsWith(caseVal)) {
|
||||||
|
matchingCase = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (matchingCase) {
|
||||||
|
if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
||||||
|
return resolveInternal(es.getExpression(), context, visited);
|
||||||
|
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) {
|
||||||
|
return resolveInternal(ys.getExpression(), context, visited);
|
||||||
|
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||||
|
return resolveInternal(rs.getExpression(), context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveExpressionWithParams(Expression expr, java.util.Map<String, String> paramValues) {
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
String name = sn.getIdentifier();
|
||||||
|
if (paramValues.containsKey(name)) return paramValues.get(name);
|
||||||
|
return null;
|
||||||
|
} else if (expr instanceof FieldAccess fa) {
|
||||||
|
String name = "this." + fa.getName().getIdentifier();
|
||||||
|
if (paramValues.containsKey(name)) return paramValues.get(name);
|
||||||
|
return null;
|
||||||
|
} else if (expr instanceof QualifiedName qn) {
|
||||||
|
return null;
|
||||||
|
} else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) {
|
||||||
|
return sl.getLiteralValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
|
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
|
||||||
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
|
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
|
||||||
|
|
||||||
@@ -134,6 +451,32 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String resolveManual(SimpleName sn, CodebaseContext context, Set<String> visited) {
|
private String resolveManual(SimpleName sn, CodebaseContext context, Set<String> visited) {
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(sn.getIdentifier())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enclosingMethod.getBody() != null) {
|
||||||
|
boolean[] isLocal = {false};
|
||||||
|
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(sn.getIdentifier())) {
|
||||||
|
isLocal[0] = true;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isLocal[0]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TypeDeclaration td = findEnclosingType(sn);
|
TypeDeclaration td = findEnclosingType(sn);
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
String fqn = context.getFqn(td);
|
String fqn = context.getFqn(td);
|
||||||
@@ -193,6 +536,28 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check constructors for assignments
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.isConstructor() && md.getBody() != null) {
|
||||||
|
for (Object stmtObj : md.getBody().statements()) {
|
||||||
|
if (stmtObj instanceof ExpressionStatement es) {
|
||||||
|
if (es.getExpression() instanceof Assignment assignment) {
|
||||||
|
Expression lhs = assignment.getLeftHandSide();
|
||||||
|
boolean matches = false;
|
||||||
|
if (lhs instanceof SimpleName snLhs && snLhs.getIdentifier().equals(fieldName)) {
|
||||||
|
matches = true;
|
||||||
|
} else if (lhs instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName) && fa.getExpression() instanceof ThisExpression) {
|
||||||
|
matches = true;
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
return resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
visited.remove(fieldId);
|
visited.remove(fieldId);
|
||||||
}
|
}
|
||||||
@@ -238,4 +603,49 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
return (TypeDeclaration) parent;
|
return (TypeDeclaration) parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null) {
|
||||||
|
if (parent instanceof MethodDeclaration md) {
|
||||||
|
return md;
|
||||||
|
}
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveLocalType(SimpleName sn) {
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
|
||||||
|
if (enclosingMethod != null && enclosingMethod.getBody() != null) {
|
||||||
|
String[] typeName = new String[1];
|
||||||
|
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
typeName[0] = node.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (typeName[0] != null) return typeName[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(sn);
|
||||||
|
if (enclosingType != null) {
|
||||||
|
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||||
|
for (Object fragObj : field.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
return field.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class CallGraphPathFinder {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
public CallGraphPathFinder(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||||
|
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||||
|
if (!visited.add(start)) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Cycle detected in call graph search: {} is already in visited path {}", start, visited);
|
||||||
|
}
|
||||||
|
return null; // Path-scoped cycle detection
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CallEdge> neighbors = graph.get(start);
|
||||||
|
if (neighbors != null) {
|
||||||
|
for (CallEdge edge : neighbors) {
|
||||||
|
String neighbor = edge.getTargetMethod();
|
||||||
|
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
||||||
|
visited.remove(start);
|
||||||
|
return new ArrayList<>(List.of(start, target));
|
||||||
|
}
|
||||||
|
List<String> path = findPath(neighbor, target, graph, visited);
|
||||||
|
if (path != null) {
|
||||||
|
path.add(0, start);
|
||||||
|
visited.remove(start);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Path search dead-end at {} when looking for {}", start, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.remove(start);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
|
for (String node : path) {
|
||||||
|
List<CallEdge> edges = callGraph.get(node);
|
||||||
|
if (edges != null) {
|
||||||
|
for (CallEdge edge : edges) {
|
||||||
|
String target = edge.getTargetMethod();
|
||||||
|
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
|
||||||
|
if (edge.getArguments().size() >= 2) {
|
||||||
|
return edge.getArguments().get(1); // The contextObj / machineId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHeuristicMatch(String neighbor, String target) {
|
||||||
|
if (neighbor == null || target == null) return false;
|
||||||
|
if (neighbor.equals(target)) return true;
|
||||||
|
|
||||||
|
if (isFullyQualifiedMethod(neighbor) && isFullyQualifiedMethod(target)) {
|
||||||
|
int lastDotNeighbor = neighbor.lastIndexOf('.');
|
||||||
|
int lastDotTarget = target.lastIndexOf('.');
|
||||||
|
if (lastDotNeighbor < 0 || lastDotTarget < 0) return false;
|
||||||
|
|
||||||
|
String methodNeighbor = neighbor.substring(lastDotNeighbor + 1);
|
||||||
|
String methodTarget = target.substring(lastDotTarget + 1);
|
||||||
|
|
||||||
|
if (!methodNeighbor.equals(methodTarget)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String classNeighbor = neighbor.substring(0, lastDotNeighbor);
|
||||||
|
String classTarget = target.substring(0, lastDotTarget);
|
||||||
|
|
||||||
|
if (classNeighbor.equals(classTarget)) return true;
|
||||||
|
|
||||||
|
String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
|
||||||
|
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
|
||||||
|
|
||||||
|
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
|
||||||
|
if (simpleClassNeighbor.contains(simpleClassTarget) || simpleClassTarget.contains(simpleClassNeighbor)) return true;
|
||||||
|
|
||||||
|
List<String> impls = context.getImplementations(classTarget);
|
||||||
|
if (impls != null) {
|
||||||
|
for (String impl : impls) {
|
||||||
|
if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> implsN = context.getImplementations(classNeighbor);
|
||||||
|
if (implsN != null) {
|
||||||
|
for (String impl : implsN) {
|
||||||
|
if (impl.equals(classTarget) || impl.endsWith("." + classTarget)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
|
||||||
|
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
||||||
|
return simpleNeighbor.equals(simpleTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFullyQualifiedMethod(String methodFqn) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return false;
|
||||||
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
|
String classPart = methodFqn.substring(0, lastDot);
|
||||||
|
if (classPart.contains(".")) return true;
|
||||||
|
if (!classPart.isEmpty() && Character.isUpperCase(classPart.charAt(0))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ConstantExtractor {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
private final Function<MethodInvocation, String> calledMethodResolver;
|
||||||
|
private VariableTracer variableTracer;
|
||||||
|
private ConstructorAnalyzer constructorAnalyzer;
|
||||||
|
|
||||||
|
public ConstantExtractor(CodebaseContext context, ConstantResolver constantResolver, Function<MethodInvocation, String> calledMethodResolver) {
|
||||||
|
this.context = context;
|
||||||
|
this.constantResolver = constantResolver;
|
||||||
|
this.calledMethodResolver = calledMethodResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVariableTracer(VariableTracer variableTracer) {
|
||||||
|
this.variableTracer = variableTracer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstructorAnalyzer(ConstructorAnalyzer constructorAnalyzer) {
|
||||||
|
this.constructorAnalyzer = constructorAnalyzer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
|
||||||
|
if (expr instanceof QualifiedName qn) {
|
||||||
|
constants.add(qn.toString());
|
||||||
|
} else if (expr instanceof StringLiteral sl) {
|
||||||
|
constants.add(sl.getLiteralValue());
|
||||||
|
} else if (expr instanceof ConditionalExpression ce) {
|
||||||
|
extractConstantsFromExpression(ce.getThenExpression(), constants);
|
||||||
|
extractConstantsFromExpression(ce.getElseExpression(), constants);
|
||||||
|
} else if (expr instanceof ParenthesizedExpression pe) {
|
||||||
|
extractConstantsFromExpression(pe.getExpression(), constants);
|
||||||
|
} else if (expr instanceof CastExpression ce) {
|
||||||
|
extractConstantsFromExpression(ce.getExpression(), constants);
|
||||||
|
} else if (expr instanceof Assignment assignment) {
|
||||||
|
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
||||||
|
} else if (expr instanceof SwitchExpression se) {
|
||||||
|
se.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(YieldStatement ys) {
|
||||||
|
extractConstantsFromExpression(ys.getExpression(), constants);
|
||||||
|
return super.visit(ys);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ExpressionStatement es) {
|
||||||
|
extractConstantsFromExpression(es.getExpression(), constants);
|
||||||
|
return super.visit(es);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement rs) {
|
||||||
|
extractConstantsFromExpression(rs.getExpression(), constants);
|
||||||
|
return super.visit(rs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (expr instanceof ArrayAccess aa) {
|
||||||
|
extractConstantsFromExpression(aa.getArray(), constants);
|
||||||
|
} else if (expr instanceof ArrayCreation ac && ac.getInitializer() != null) {
|
||||||
|
for (Object expObj : ac.getInitializer().expressions()) {
|
||||||
|
extractConstantsFromArgument((Expression) expObj, constants);
|
||||||
|
}
|
||||||
|
} else if (expr instanceof ArrayInitializer ai) {
|
||||||
|
for (Object expObj : ai.expressions()) {
|
||||||
|
extractConstantsFromArgument((Expression) expObj, constants);
|
||||||
|
}
|
||||||
|
} else if (expr instanceof MethodInvocation mi) {
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
|
||||||
|
System.out.println("Processing mi: " + mi); if (methodName.equals("get") || methodName.equals("getOrDefault")) {
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
if (variableTracer != null) {
|
||||||
|
List<Expression> traced = variableTracer.traceVariableAll(mi.getExpression());
|
||||||
|
for (Expression t : traced) {
|
||||||
|
extractConstantsFromExpression(t, constants);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extractConstantsFromExpression(mi.getExpression(), constants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry") ||
|
||||||
|
methodName.equals("just") || methodName.equals("withPayload") || methodName.equals("success")) {
|
||||||
|
for (Object argObj : mi.arguments()) {
|
||||||
|
extractConstantsFromExpression((Expression) argObj, constants);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof SimpleName sn) {
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
String propName = methodName.startsWith("get") ? methodName.substring(3) : methodName;
|
||||||
|
|
||||||
|
Block block = findEnclosingBlock(mi);
|
||||||
|
if (block != null) {
|
||||||
|
for (Object stmtObj : block.statements()) {
|
||||||
|
if (stmtObj == mi.getParent() || stmtObj == mi) break;
|
||||||
|
if (stmtObj instanceof ExpressionStatement es) {
|
||||||
|
if (es.getExpression() instanceof MethodInvocation setterMi) {
|
||||||
|
if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
if (setterMi.getExpression() instanceof SimpleName setterSn && setterSn.getIdentifier().equals(varName)) {
|
||||||
|
if (!setterMi.arguments().isEmpty()) {
|
||||||
|
extractConstantsFromExpression((Expression) setterMi.arguments().get(0), constants);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (es.getExpression() instanceof Assignment assignment) {
|
||||||
|
if (assignment.getLeftHandSide() instanceof FieldAccess fa) {
|
||||||
|
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
||||||
|
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (assignment.getLeftHandSide() instanceof QualifiedName qqn) {
|
||||||
|
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
||||||
|
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Expression> receivers = new ArrayList<>();
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
if (mi.getExpression() instanceof ClassInstanceCreation) {
|
||||||
|
receivers.add(mi.getExpression());
|
||||||
|
} else if (variableTracer != null) {
|
||||||
|
receivers.addAll(variableTracer.traceVariableAll(mi.getExpression()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Expression receiverExpr : receivers) {
|
||||||
|
if (receiverExpr instanceof ClassInstanceCreation cic) {
|
||||||
|
if (cic.getAnonymousClassDeclaration() != null) {
|
||||||
|
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||||
|
if (declObj instanceof MethodDeclaration mdecl) {
|
||||||
|
if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) {
|
||||||
|
mdecl.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement rs) {
|
||||||
|
if (rs.getExpression() != null) {
|
||||||
|
extractConstantsFromExpression(rs.getExpression(), constants);
|
||||||
|
}
|
||||||
|
return super.visit(rs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (constructorAnalyzer != null) {
|
||||||
|
boolean handledByLombok = false;
|
||||||
|
if (methodName.startsWith("get") && methodName.length() > 3) {
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
TypeDeclaration typeDecl = context.getTypeDeclaration(typeName);
|
||||||
|
if (typeDecl != null) {
|
||||||
|
String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
|
||||||
|
int fieldIndex = constructorAnalyzer.findConstructorArgumentIndexForLombokField(typeDecl, fieldName);
|
||||||
|
if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) {
|
||||||
|
extractConstantsFromArgument((Expression) cic.arguments().get(fieldIndex), constants);
|
||||||
|
handledByLombok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handledByLombok && methodName != null && !methodName.isEmpty()) {
|
||||||
|
List<String> narrowed = constructorAnalyzer.resolveInlineInstantiationGetterResult(
|
||||||
|
cic, methodName, context, new HashSet<>());
|
||||||
|
if (narrowed.isEmpty()) {
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
CompilationUnit contextCu = expr.getRoot() instanceof CompilationUnit ? (CompilationUnit) expr.getRoot() : null;
|
||||||
|
TypeDeclaration typeDecl = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName);
|
||||||
|
if (typeDecl == null) typeDecl = context.getTypeDeclaration(typeName);
|
||||||
|
if (typeDecl != null) {
|
||||||
|
narrowed = resolveMethodReturnConstant(context.getFqn(typeDecl), methodName, 0, new HashSet<>(), contextCu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (narrowed != null && !narrowed.isEmpty()) {
|
||||||
|
constants.addAll(narrowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression currentMi = mi;
|
||||||
|
while (currentMi instanceof MethodInvocation chainMi) {
|
||||||
|
String chainName = chainMi.getName().getIdentifier();
|
||||||
|
if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) {
|
||||||
|
extractConstantsFromExpression((Expression) chainMi.arguments().get(1), constants);
|
||||||
|
} else if ((chainName.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) {
|
||||||
|
extractConstantsFromExpression((Expression) chainMi.arguments().get(0), constants);
|
||||||
|
}
|
||||||
|
currentMi = chainMi.getExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration td = findEnclosingType(mi);
|
||||||
|
if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof ThisExpression)) {
|
||||||
|
List<String> values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new HashSet<>(), null);
|
||||||
|
if (values != null) constants.addAll(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void extractConstantsFromArgument(Expression argObj, List<String> constants) {
|
||||||
|
if (argObj instanceof ClassInstanceCreation innerCic) {
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
|
||||||
|
if ("String".equals(typeName)) {
|
||||||
|
extractConstantsFromExpression(argObj, constants);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extractConstantsFromExpression(argObj, constants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) {
|
||||||
|
if (className != null && className.contains(".")) {
|
||||||
|
String prefix = className.substring(0, className.lastIndexOf('.'));
|
||||||
|
String suffix = className.substring(className.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration prefixTd = contextCu != null ? context.getTypeDeclaration(prefix, contextCu) : context.getTypeDeclaration(prefix);
|
||||||
|
if (prefixTd == null) prefixTd = context.getTypeDeclaration(prefix);
|
||||||
|
boolean isKnownType = prefixTd != null || context.getEnumValues(prefix) != null;
|
||||||
|
if (isKnownType) {
|
||||||
|
String simplePrefix = prefix.contains(".") ? prefix.substring(prefix.lastIndexOf('.') + 1) : prefix;
|
||||||
|
return Collections.singletonList(simplePrefix + "." + suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
|
||||||
|
if (td == null) td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) return null;
|
||||||
|
|
||||||
|
final List<String> resolvedConstants = new ArrayList<>();
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
if (variableTracer != null) {
|
||||||
|
List<Expression> tracedReturns = variableTracer.traceVariableAll(node.getExpression());
|
||||||
|
for (Expression tracedRet : tracedReturns) {
|
||||||
|
extractConstantsFromExpression(tracedRet, resolvedConstants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!resolvedConstants.isEmpty()) {
|
||||||
|
return resolvedConstants;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
||||||
|
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
||||||
|
if (depth > 20) {
|
||||||
|
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String fqn = className + "." + methodName;
|
||||||
|
if (!visited.add(fqn)) {
|
||||||
|
log.debug("Exiting resolveMethodReturnConstant early (cycle detected) for fqn={}", fqn);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||||
|
if (tempTd == null) {
|
||||||
|
tempTd = context.getTypeDeclaration(className);
|
||||||
|
}
|
||||||
|
final TypeDeclaration td = tempTd;
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
Expression retExpr = node.getExpression();
|
||||||
|
if (retExpr != null) {
|
||||||
|
boolean handled = false;
|
||||||
|
if (retExpr instanceof MethodInvocation mi && calledMethodResolver != null) {
|
||||||
|
String called = calledMethodResolver.apply(mi);
|
||||||
|
if (called != null && called.contains(".")) {
|
||||||
|
if (visited.contains(called)) {
|
||||||
|
handled = true;
|
||||||
|
} else {
|
||||||
|
String cName = called.substring(0, called.lastIndexOf('.'));
|
||||||
|
String mName = called.substring(called.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
|
||||||
|
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||||
|
|
||||||
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
|
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||||
|
constants.addAll(delegationResult);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (retExpr instanceof SuperMethodInvocation smi) {
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null) {
|
||||||
|
String called = superFqn + "." + smi.getName().getIdentifier();
|
||||||
|
if (visited.contains(called)) {
|
||||||
|
handled = true;
|
||||||
|
} else {
|
||||||
|
String cName = superFqn;
|
||||||
|
String mName = smi.getName().getIdentifier();
|
||||||
|
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
|
||||||
|
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||||
|
|
||||||
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
|
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||||
|
constants.addAll(delegationResult);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled && variableTracer != null && constructorAnalyzer != null) {
|
||||||
|
List<Expression> tracedRetExprs = variableTracer.traceVariableAll(retExpr);
|
||||||
|
for (Expression tracedRetExpr : tracedRetExprs) {
|
||||||
|
extractConstantsFromExpression(tracedRetExpr, constants);
|
||||||
|
String val = constantResolver.resolve(tracedRetExpr, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
String parsed = parseEnumSetElement(eVal);
|
||||||
|
if (!constants.contains(parsed)) constants.add(parsed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!constants.contains(val)) constants.add(val);
|
||||||
|
}
|
||||||
|
} else if (tracedRetExpr instanceof SimpleName sn) {
|
||||||
|
List<String> consts = constructorAnalyzer.traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
constants.addAll(consts);
|
||||||
|
}
|
||||||
|
} else if (tracedRetExpr instanceof FieldAccess fa) {
|
||||||
|
List<String> consts = constructorAnalyzer.traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited);
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
constants.addAll(consts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
List<String> impls = context.getImplementations(className);
|
||||||
|
if (impls != null) {
|
||||||
|
for (String implName : impls) {
|
||||||
|
List<String> delegationResult = resolveMethodReturnConstant(implName, methodName, depth + 1, visited, contextCu);
|
||||||
|
constants.addAll(delegationResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited.remove(fqn);
|
||||||
|
log.debug("resolveMethodReturnConstant for class={}, method={} resolved constants: {}", className, methodName, constants);
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String parseEnumSetElement(String eVal) {
|
||||||
|
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Block findEnclosingBlock(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof Block)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (Block) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (TypeDeclaration) parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,808 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class ConstructorAnalyzer {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
private VariableTracer variableTracer;
|
||||||
|
private ConstantExtractor constantExtractor;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface RhsResolver {
|
||||||
|
String resolve(Expression rhs, Map<String, String> paramValues, TypeDeclaration td);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConstructorAnalyzer(CodebaseContext context, ConstantResolver constantResolver) {
|
||||||
|
this.context = context;
|
||||||
|
this.constantResolver = constantResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVariableTracer(VariableTracer variableTracer) {
|
||||||
|
this.variableTracer = variableTracer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstantExtractor(ConstantExtractor constantExtractor) {
|
||||||
|
this.constantExtractor = constantExtractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
|
if (td == null || fieldName == null) return Collections.emptyList();
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (fqn == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
String cycleKey = fqn + "#" + fieldName;
|
||||||
|
if (!visited.add(cycleKey)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
|
||||||
|
Expression right = frag.getInitializer();
|
||||||
|
if (variableTracer != null && constantExtractor != null) {
|
||||||
|
List<Expression> tracedRights = variableTracer.traceVariableAll(right);
|
||||||
|
for (Expression tracedRight : tracedRights) {
|
||||||
|
if (tracedRight instanceof MethodInvocation mi) {
|
||||||
|
String calledName = mi.getName().getIdentifier();
|
||||||
|
String targetClass = context.getFqn(td);
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
if (mi.getExpression() == null) {
|
||||||
|
TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu);
|
||||||
|
if (targetTd != null) {
|
||||||
|
targetClass = context.getFqn(targetTd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(tracedRight, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ASTVisitor assignmentVisitor = new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression left = node.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
|
||||||
|
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
|
||||||
|
|
||||||
|
Expression right = node.getRightHandSide();
|
||||||
|
if (variableTracer != null && constantExtractor != null) {
|
||||||
|
List<Expression> tracedRights = variableTracer.traceVariableAll(right);
|
||||||
|
for (Expression tracedRight : tracedRights) {
|
||||||
|
if (tracedRight instanceof MethodInvocation mi) {
|
||||||
|
String calledName = mi.getName().getIdentifier();
|
||||||
|
String targetClass = context.getFqn(td);
|
||||||
|
|
||||||
|
if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
|
||||||
|
targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
|
||||||
|
} else if (mi.getExpression() instanceof SimpleName) {
|
||||||
|
String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
|
||||||
|
if (targetTd != null) {
|
||||||
|
targetClass = context.getFqn(targetTd);
|
||||||
|
}
|
||||||
|
} else if (mi.getExpression() == null) {
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu);
|
||||||
|
if (targetTd != null) {
|
||||||
|
targetClass = context.getFqn(targetTd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(tracedRight, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation node) {
|
||||||
|
processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited);
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation node) {
|
||||||
|
TypeDeclaration enclosingTd = findEnclosingType(node);
|
||||||
|
if (enclosingTd != null) {
|
||||||
|
String superFqn = context.getSuperclassFqn(enclosingTd);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.isConstructor() && md.getBody() != null) {
|
||||||
|
md.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object bodyDecl : td.bodyDeclarations()) {
|
||||||
|
if (bodyDecl instanceof Initializer init && init.getBody() != null) {
|
||||||
|
init.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
// Context-Aware Fallback (Up): Check superclass constructors
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
if (superTd != null) {
|
||||||
|
results.addAll(traceFieldInConstructors(superTd, fieldName, context, visited));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
// Context-Aware Fallback (Down): Check subclasses/implementations
|
||||||
|
List<String> impls = context.getImplementations(fqn);
|
||||||
|
if (impls != null) {
|
||||||
|
for (String implFqn : impls) {
|
||||||
|
TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
|
||||||
|
if (implTd != null) {
|
||||||
|
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
// Global Fallback: Search all parsed classes in the workspace
|
||||||
|
Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
|
||||||
|
if (allTypes != null) {
|
||||||
|
int matchedCount = 0;
|
||||||
|
for (TypeDeclaration typeDecl : allTypes) {
|
||||||
|
if (typeDecl == null) continue;
|
||||||
|
if (context.hasField(typeDecl, fieldName)) {
|
||||||
|
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited));
|
||||||
|
matchedCount++;
|
||||||
|
if (matchedCount >= 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.remove(cycleKey);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildFieldValuesFromCIC(
|
||||||
|
TypeDeclaration td,
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
CodebaseContext context,
|
||||||
|
RhsResolver rhsResolver,
|
||||||
|
Map<String, String> initialLocals) {
|
||||||
|
|
||||||
|
String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName);
|
||||||
|
if (actualTd == null) {
|
||||||
|
actualTd = td;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> fieldValues = new HashMap<>();
|
||||||
|
Set<String> visitedCtors = new HashSet<>();
|
||||||
|
buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors, rhsResolver, initialLocals);
|
||||||
|
return fieldValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildFieldValuesFromCIC(
|
||||||
|
TypeDeclaration td,
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
CodebaseContext context,
|
||||||
|
RhsResolver rhsResolver) {
|
||||||
|
return buildFieldValuesFromCIC(td, cic, context, rhsResolver, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildFieldValuesFromCIC(
|
||||||
|
TypeDeclaration td,
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
CodebaseContext context) {
|
||||||
|
return buildFieldValuesFromCIC(td, cic, context, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildFieldValuesFromCICRecursive(
|
||||||
|
TypeDeclaration td,
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
CodebaseContext context,
|
||||||
|
Map<String, String> fieldValues,
|
||||||
|
Set<String> visitedCtors,
|
||||||
|
RhsResolver rhsResolver,
|
||||||
|
Map<String, String> initialLocals) {
|
||||||
|
|
||||||
|
String tdKey = context.getFqn(td);
|
||||||
|
if (!visitedCtors.add(tdKey)) return;
|
||||||
|
|
||||||
|
IMethodBinding resolvedCtor = cic.resolveConstructorBinding();
|
||||||
|
|
||||||
|
for (MethodDeclaration ctor : td.getMethods()) {
|
||||||
|
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
|
||||||
|
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedCtor != null && ctor.resolveBinding() != null) {
|
||||||
|
matches = resolvedCtor.isEqualTo(ctor.resolveBinding()) || resolvedCtor.getKey().equals(ctor.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = ctor.parameters().size() == cic.arguments().size();
|
||||||
|
}
|
||||||
|
if (!matches) continue;
|
||||||
|
|
||||||
|
Map<String, String> paramValues = new HashMap<>();
|
||||||
|
for (int i = 0; i < ctor.parameters().size(); i++) {
|
||||||
|
String pName = ((SingleVariableDeclaration) ctor.parameters().get(i)).getName().getIdentifier();
|
||||||
|
Expression argExpr = (Expression) cic.arguments().get(i);
|
||||||
|
List<String> consts = new ArrayList<>();
|
||||||
|
if (constantExtractor != null) {
|
||||||
|
constantExtractor.extractConstantsFromArgument(argExpr, consts);
|
||||||
|
}
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
paramValues.put(pName, consts.get(0));
|
||||||
|
} else {
|
||||||
|
String resolved = null;
|
||||||
|
if (argExpr instanceof SimpleName sn && initialLocals != null && initialLocals.containsKey(sn.getIdentifier())) {
|
||||||
|
resolved = initialLocals.get(sn.getIdentifier());
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
resolved = constantResolver.resolve(argExpr, context);
|
||||||
|
}
|
||||||
|
if (resolved != null) paramValues.put(pName, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paramValues.isEmpty()) continue;
|
||||||
|
|
||||||
|
ctor.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression lhs = node.getLeftHandSide();
|
||||||
|
Expression rhs = node.getRightHandSide();
|
||||||
|
|
||||||
|
String fieldName = null;
|
||||||
|
if (lhs instanceof FieldAccess fa
|
||||||
|
&& fa.getExpression() instanceof ThisExpression) {
|
||||||
|
fieldName = fa.getName().getIdentifier();
|
||||||
|
} else if (lhs instanceof SimpleName sn
|
||||||
|
&& !paramValues.containsKey(sn.getIdentifier())) {
|
||||||
|
fieldName = sn.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldName != null) {
|
||||||
|
String paramVal = null;
|
||||||
|
if (rhs instanceof SimpleName snRhs) {
|
||||||
|
paramVal = paramValues.get(snRhs.getIdentifier());
|
||||||
|
} else if (rhsResolver != null) {
|
||||||
|
paramVal = rhsResolver.resolve(rhs, paramValues, td);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramVal != null) {
|
||||||
|
fieldValues.put(fieldName, paramVal);
|
||||||
|
fieldValues.put("this." + fieldName, paramVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctor.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation sci) {
|
||||||
|
IMethodBinding binding = sci.resolveConstructorBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
|
||||||
|
if (superTd != null) {
|
||||||
|
processSuperConstructorArgs(superTd, binding, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
if (superTd != null) {
|
||||||
|
processSuperConstructorArgs(superTd, null, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation ci) {
|
||||||
|
IMethodBinding binding = ci.resolveConstructorBinding();
|
||||||
|
processSuperConstructorArgs(td, binding, ci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(ci);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processSuperConstructorArgs(
|
||||||
|
TypeDeclaration superTd,
|
||||||
|
IMethodBinding resolvedBinding,
|
||||||
|
List<?> superArgs,
|
||||||
|
Map<String, String> subClassParamValues,
|
||||||
|
CodebaseContext context,
|
||||||
|
Map<String, String> fieldValues,
|
||||||
|
Set<String> visitedCtors,
|
||||||
|
RhsResolver rhsResolver) {
|
||||||
|
|
||||||
|
if (superTd == null) return;
|
||||||
|
|
||||||
|
for (MethodDeclaration superCtor : superTd.getMethods()) {
|
||||||
|
if (!superCtor.isConstructor() || superCtor.getBody() == null) continue;
|
||||||
|
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && superCtor.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(superCtor.resolveBinding()) || resolvedBinding.getKey().equals(superCtor.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = superCtor.parameters().size() == superArgs.size();
|
||||||
|
}
|
||||||
|
if (!matches) continue;
|
||||||
|
|
||||||
|
Map<String, String> superParamValues = new HashMap<>();
|
||||||
|
for (int i = 0; i < superCtor.parameters().size(); i++) {
|
||||||
|
String pName = ((SingleVariableDeclaration) superCtor.parameters().get(i)).getName().getIdentifier();
|
||||||
|
Expression argExpr = (Expression) superArgs.get(i);
|
||||||
|
|
||||||
|
String resolved = null;
|
||||||
|
if (argExpr instanceof SimpleName sn) {
|
||||||
|
resolved = subClassParamValues.get(sn.getIdentifier());
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
List<String> consts = new ArrayList<>();
|
||||||
|
if (constantExtractor != null) {
|
||||||
|
constantExtractor.extractConstantsFromArgument(argExpr, consts);
|
||||||
|
}
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
resolved = consts.get(0);
|
||||||
|
} else {
|
||||||
|
resolved = constantResolver.resolve(argExpr, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
superParamValues.put(pName, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (superParamValues.isEmpty()) continue;
|
||||||
|
|
||||||
|
superCtor.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression lhs = node.getLeftHandSide();
|
||||||
|
Expression rhs = node.getRightHandSide();
|
||||||
|
|
||||||
|
String fieldName = null;
|
||||||
|
if (lhs instanceof FieldAccess fa
|
||||||
|
&& fa.getExpression() instanceof ThisExpression) {
|
||||||
|
fieldName = fa.getName().getIdentifier();
|
||||||
|
} else if (lhs instanceof SimpleName sn
|
||||||
|
&& !superParamValues.containsKey(sn.getIdentifier())) {
|
||||||
|
fieldName = sn.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldName != null) {
|
||||||
|
String paramVal = null;
|
||||||
|
if (rhs instanceof SimpleName snRhs) {
|
||||||
|
paramVal = superParamValues.get(snRhs.getIdentifier());
|
||||||
|
} else if (rhsResolver != null) {
|
||||||
|
paramVal = rhsResolver.resolve(rhs, superParamValues, superTd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramVal != null) {
|
||||||
|
fieldValues.put(fieldName, paramVal);
|
||||||
|
fieldValues.put("this." + fieldName, paramVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
superCtor.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation sci) {
|
||||||
|
IMethodBinding binding = sci.resolveConstructorBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
TypeDeclaration nextSuperTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
|
||||||
|
if (nextSuperTd != null) {
|
||||||
|
processSuperConstructorArgs(nextSuperTd, binding, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String superFqn = context.getSuperclassFqn(superTd);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
|
||||||
|
if (nextSuperTd != null) {
|
||||||
|
processSuperConstructorArgs(nextSuperTd, null, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation ci) {
|
||||||
|
IMethodBinding binding = ci.resolveConstructorBinding();
|
||||||
|
processSuperConstructorArgs(superTd, binding, ci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(ci);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String evaluateMethodCallInConstructor(
|
||||||
|
Expression rhs,
|
||||||
|
Map<String, String> paramValues,
|
||||||
|
TypeDeclaration td) {
|
||||||
|
|
||||||
|
if (!(rhs instanceof MethodInvocation mi)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if (receiver != null && !(receiver instanceof ThisExpression)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration method = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (method == null || method.getBody() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> localVars = new HashMap<>(paramValues);
|
||||||
|
return constantResolver.evaluateMethodBodyWithLocals(method, localVars, context, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClassInstanceCreation findReturnedCIC(
|
||||||
|
String className, String methodName, CodebaseContext context) {
|
||||||
|
return findReturnedCIC(className, methodName, context, new HashSet<>(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClassInstanceCreation findReturnedCIC(
|
||||||
|
String className, String methodName, CodebaseContext context,
|
||||||
|
Set<String> visited, int depth) {
|
||||||
|
if (depth > 50 || !visited.add(className + "#" + methodName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
ClassInstanceCreation[] result = {null};
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement rs) {
|
||||||
|
if (rs.getExpression() instanceof ClassInstanceCreation cic) {
|
||||||
|
result[0] = cic;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (result[0] != null) return result[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> impls = context.getImplementations(className);
|
||||||
|
for (String impl : impls) {
|
||||||
|
ClassInstanceCreation cic = findReturnedCIC(impl, methodName, context, visited, depth + 1);
|
||||||
|
if (cic != null) return cic;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveInlineInstantiationGetterResult(
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
String getterName,
|
||||||
|
CodebaseContext context,
|
||||||
|
Set<String> visited) {
|
||||||
|
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
CompilationUnit contextCu = cic.getRoot() instanceof CompilationUnit ? (CompilationUnit) cic.getRoot() : null;
|
||||||
|
AbstractTypeDeclaration atd = contextCu != null ? context.getAbstractTypeDeclaration(typeName, contextCu) : context.getAbstractTypeDeclaration(typeName);
|
||||||
|
if (atd == null) atd = context.getAbstractTypeDeclaration(typeName);
|
||||||
|
if (atd == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
if (atd instanceof RecordDeclaration rd) {
|
||||||
|
int compIdx = -1;
|
||||||
|
List<?> components = rd.recordComponents();
|
||||||
|
for (int i = 0; i < components.size(); i++) {
|
||||||
|
Object compObj = components.get(i);
|
||||||
|
if (compObj != null) {
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method getNameMethod = compObj.getClass().getMethod("getName");
|
||||||
|
SimpleName nameObj = (SimpleName) getNameMethod.invoke(compObj);
|
||||||
|
if (nameObj != null && nameObj.getIdentifier().equals(getterName)) {
|
||||||
|
compIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore reflection errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (compIdx >= 0 && compIdx < cic.arguments().size()) {
|
||||||
|
Expression argExpr = (Expression) cic.arguments().get(compIdx);
|
||||||
|
List<String> consts = new ArrayList<>();
|
||||||
|
if (constantExtractor != null) {
|
||||||
|
constantExtractor.extractConstantsFromArgument(argExpr, consts);
|
||||||
|
}
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
return consts;
|
||||||
|
} else {
|
||||||
|
String resolved = constantResolver.resolve(argExpr, context);
|
||||||
|
if (resolved != null) {
|
||||||
|
return List.of(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(atd instanceof TypeDeclaration td)) return Collections.emptyList();
|
||||||
|
|
||||||
|
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
|
||||||
|
if (getter == null || getter.getBody() == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
String getterKey = typeName + "#" + getterName;
|
||||||
|
if (!visited.add(getterKey)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context);
|
||||||
|
if (fieldValues.isEmpty()) return Collections.emptyList();
|
||||||
|
|
||||||
|
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
||||||
|
return result != null ? List.of(result) : Collections.emptyList();
|
||||||
|
} finally {
|
||||||
|
visited.remove(getterKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) {
|
||||||
|
boolean isAllArgsConstructor = false;
|
||||||
|
boolean isRequiredArgsConstructor = false;
|
||||||
|
boolean isData = false;
|
||||||
|
boolean isValue = false;
|
||||||
|
|
||||||
|
for (Object modObj : td.modifiers()) {
|
||||||
|
if (modObj instanceof MarkerAnnotation ma) {
|
||||||
|
String name = ma.getTypeName().getFullyQualifiedName();
|
||||||
|
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
|
||||||
|
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
|
||||||
|
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
|
||||||
|
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
|
||||||
|
} else if (modObj instanceof NormalAnnotation na) {
|
||||||
|
String name = na.getTypeName().getFullyQualifiedName();
|
||||||
|
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
|
||||||
|
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
|
||||||
|
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
|
||||||
|
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAllArgsConstructor && !isRequiredArgsConstructor && !isData && !isValue) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
boolean isStatic = false;
|
||||||
|
boolean isFinal = false;
|
||||||
|
boolean hasNonNull = false;
|
||||||
|
for (Object modObj : fd.modifiers()) {
|
||||||
|
if (modObj instanceof Modifier mod) {
|
||||||
|
if (mod.isStatic()) isStatic = true;
|
||||||
|
if (mod.isFinal()) isFinal = true;
|
||||||
|
} else if (modObj instanceof MarkerAnnotation ma) {
|
||||||
|
if (ma.getTypeName().getFullyQualifiedName().endsWith("NonNull")) hasNonNull = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isStatic) continue;
|
||||||
|
|
||||||
|
if (isRequiredArgsConstructor && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
|
||||||
|
if (isData && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
|
||||||
|
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(fieldName)) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processConstructorInvocationArgs(List<?> arguments, TypeDeclaration targetTd, ASTNode callNode, List<String> results, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
|
if (targetTd == null) return;
|
||||||
|
MethodDeclaration callerMd = findEnclosingMethod(callNode);
|
||||||
|
|
||||||
|
IMethodBinding resolvedBinding = null;
|
||||||
|
if (callNode instanceof ConstructorInvocation ci) {
|
||||||
|
resolvedBinding = ci.resolveConstructorBinding();
|
||||||
|
} else if (callNode instanceof SuperConstructorInvocation sci) {
|
||||||
|
resolvedBinding = sci.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MethodDeclaration otherMd : targetTd.getMethods()) {
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new HashSet<>());
|
||||||
|
if (targetIdx >= 0 && targetIdx < arguments.size()) {
|
||||||
|
Expression arg = (Expression) arguments.get(targetIdx);
|
||||||
|
String val = constantResolver.resolve(arg, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
if (constantExtractor != null) {
|
||||||
|
results.add(constantExtractor.parseEnumSetElement(eVal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, Set<MethodDeclaration> visited) {
|
||||||
|
if (constructorMd == null || constructorMd.getBody() == null) return -1;
|
||||||
|
if (!visited.add(constructorMd)) return -1;
|
||||||
|
|
||||||
|
final int[] foundIdx = {-1};
|
||||||
|
constructorMd.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment asn) {
|
||||||
|
Expression left = asn.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) ||
|
||||||
|
(left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) ||
|
||||||
|
(left instanceof SuperFieldAccess sfa && sfa.getName().getIdentifier().equals(fieldName))) {
|
||||||
|
Expression right = asn.getRightHandSide();
|
||||||
|
String rightName = extractVariableName(right);
|
||||||
|
if (rightName != null) {
|
||||||
|
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||||
|
if (svd.getName().getIdentifier().equals(rightName)) {
|
||||||
|
foundIdx[0] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(asn);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation node) {
|
||||||
|
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
|
||||||
|
TypeDeclaration enclosingTd = findEnclosingType(node);
|
||||||
|
if (enclosingTd != null) {
|
||||||
|
for (MethodDeclaration otherMd : enclosingTd.getMethods()) {
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size();
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
|
||||||
|
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
|
||||||
|
Expression arg = (Expression) node.arguments().get(targetIdx);
|
||||||
|
String argName = extractVariableName(arg);
|
||||||
|
if (argName != null) {
|
||||||
|
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||||
|
if (svd.getName().getIdentifier().equals(argName)) {
|
||||||
|
foundIdx[0] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation node) {
|
||||||
|
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
|
||||||
|
TypeDeclaration enclosingTd = findEnclosingType(node);
|
||||||
|
if (enclosingTd != null) {
|
||||||
|
String superFqn = context.getSuperclassFqn(enclosingTd);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
if (superTd != null) {
|
||||||
|
for (MethodDeclaration otherMd : superTd.getMethods()) {
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size();
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
|
||||||
|
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
|
||||||
|
Expression arg = (Expression) node.arguments().get(targetIdx);
|
||||||
|
String argName = extractVariableName(arg);
|
||||||
|
if (argName != null) {
|
||||||
|
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||||
|
if (svd.getName().getIdentifier().equals(argName)) {
|
||||||
|
foundIdx[0] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return foundIdx[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractVariableName(Expression expr) {
|
||||||
|
if (expr instanceof SimpleName sn) return sn.getIdentifier();
|
||||||
|
if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier();
|
||||||
|
if (expr instanceof SuperFieldAccess sfa) return sfa.getName().getIdentifier();
|
||||||
|
if (expr instanceof CastExpression ce) return extractVariableName(ce.getExpression());
|
||||||
|
if (expr instanceof ParenthesizedExpression pe) return extractVariableName(pe.getExpression());
|
||||||
|
if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
|
||||||
|
if (!mi.arguments().isEmpty()) return extractVariableName((Expression)mi.arguments().get(0));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (MethodDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (TypeDeclaration) parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,16 @@ public class GenericEventDetector {
|
|||||||
if ("sendEvent".equals(methodName) || "sendEvents".equals(methodName) ||
|
if ("sendEvent".equals(methodName) || "sendEvents".equals(methodName) ||
|
||||||
"sendEventCollect".equals(methodName) || "sendEventMono".equals(methodName)) {
|
"sendEventCollect".equals(methodName) || "sendEventMono".equals(methodName)) {
|
||||||
processSendEvent(node, cu, triggers);
|
processSendEvent(node, cu, triggers);
|
||||||
|
} else {
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
if (argObj instanceof ExpressionMethodReference emr) {
|
||||||
|
String refMethod = emr.getName().getIdentifier();
|
||||||
|
if ("sendEvent".equals(refMethod) || "sendEvents".equals(refMethod) ||
|
||||||
|
"sendEventCollect".equals(refMethod) || "sendEventMono".equals(refMethod)) {
|
||||||
|
processSendEventMethodReference(node, emr, cu, triggers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processHints(node, cu, triggers);
|
processHints(node, cu, triggers);
|
||||||
@@ -46,6 +56,81 @@ public class GenericEventDetector {
|
|||||||
return triggers;
|
return triggers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void processSendEventMethodReference(MethodInvocation node, ExpressionMethodReference emr, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||||
|
Expression receiver = node.getExpression();
|
||||||
|
String eventValue = extractEventFromReceiver(receiver);
|
||||||
|
if (eventValue == null) {
|
||||||
|
eventValue = receiver != null ? receiver.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
|
TypeDeclaration type = findEnclosingType(node);
|
||||||
|
if (type == null) return;
|
||||||
|
|
||||||
|
String sourceState = extractSourceState(node);
|
||||||
|
String[] smTypes = resolveStateMachineTypeArgumentsForExpression(emr.getExpression(), node);
|
||||||
|
|
||||||
|
if (eventValue != null) {
|
||||||
|
triggers.add(TriggerPoint.builder()
|
||||||
|
.event(eventValue)
|
||||||
|
.sourceState(sourceState)
|
||||||
|
.className(context.getFqn(type))
|
||||||
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.stateTypeFqn(smTypes[0])
|
||||||
|
.eventTypeFqn(smTypes[1])
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractEventFromReceiver(Expression receiver) {
|
||||||
|
if (receiver == null) return null;
|
||||||
|
if (receiver instanceof MethodInvocation mi) {
|
||||||
|
String mName = mi.getName().getIdentifier();
|
||||||
|
if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
String resolved = constantResolver.resolve(arg, context);
|
||||||
|
return resolved != null ? resolved : arg.toString();
|
||||||
|
}
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
return extractEventFromReceiver(mi.getExpression());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] resolveStateMachineTypeArgumentsForExpression(Expression receiver, MethodInvocation node) {
|
||||||
|
if (receiver == null) return new String[]{null, null};
|
||||||
|
|
||||||
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding smBinding = findStateMachineSupertype(binding, new java.util.HashSet<>());
|
||||||
|
if (smBinding != null) {
|
||||||
|
ITypeBinding[] typeArgs = smBinding.getTypeArguments();
|
||||||
|
if (typeArgs.length >= 2) {
|
||||||
|
return new String[]{typeArgs[0].getQualifiedName(), typeArgs[1].getQualifiedName()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Type declType = findReceiverTypeManually(receiver, node);
|
||||||
|
if (declType instanceof ParameterizedType pt) {
|
||||||
|
List<?> typeArgs = pt.typeArguments();
|
||||||
|
if (typeArgs.size() >= 2) {
|
||||||
|
CompilationUnit compilationUnit = (CompilationUnit) node.getRoot();
|
||||||
|
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), compilationUnit);
|
||||||
|
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), compilationUnit);
|
||||||
|
return new String[]{stateType, eventType};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||||
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
|
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
|
||||||
if (builtTriggers != null) {
|
if (builtTriggers != null) {
|
||||||
@@ -141,6 +226,7 @@ public class GenericEventDetector {
|
|||||||
if (type == null) return Collections.emptyList();
|
if (type == null) return Collections.emptyList();
|
||||||
|
|
||||||
String sourceState = extractSourceState(node);
|
String sourceState = extractSourceState(node);
|
||||||
|
String[] smTypes = resolveStateMachineTypeArguments(node);
|
||||||
|
|
||||||
List<TriggerPoint> results = new ArrayList<>();
|
List<TriggerPoint> results = new ArrayList<>();
|
||||||
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
||||||
@@ -154,6 +240,8 @@ public class GenericEventDetector {
|
|||||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.stateTypeFqn(smTypes[0])
|
||||||
|
.eventTypeFqn(smTypes[1])
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,6 +253,8 @@ public class GenericEventDetector {
|
|||||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.stateTypeFqn(smTypes[0])
|
||||||
|
.eventTypeFqn(smTypes[1])
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,8 +355,26 @@ public class GenericEventDetector {
|
|||||||
return getSimpleNameString(right);
|
return getSimpleNameString(right);
|
||||||
}
|
}
|
||||||
} else if (expr instanceof MethodInvocation mi) {
|
} else if (expr instanceof MethodInvocation mi) {
|
||||||
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
|
String methodName = mi.getName().getIdentifier();
|
||||||
return getSimpleNameString((Expression) mi.arguments().get(0));
|
if (("equals".equals(methodName) || "equalsIgnoreCase".equals(methodName)) && !mi.arguments().isEmpty()) {
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
|
||||||
|
// If receiver is null (e.g., implicit this), fall back to arg
|
||||||
|
if (receiver == null) {
|
||||||
|
return getSimpleNameString(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize the one that looks like a constant (QualifiedName or StringLiteral)
|
||||||
|
if (receiver instanceof QualifiedName || receiver instanceof StringLiteral || receiver instanceof FieldAccess) {
|
||||||
|
return getSimpleNameString(receiver);
|
||||||
|
}
|
||||||
|
if (arg instanceof QualifiedName || arg instanceof StringLiteral || arg instanceof FieldAccess) {
|
||||||
|
return getSimpleNameString(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to receiver
|
||||||
|
return getSimpleNameString(receiver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -392,4 +500,139 @@ public class GenericEventDetector {
|
|||||||
}
|
}
|
||||||
return (TypeDeclaration) parent;
|
return (TypeDeclaration) parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String[] resolveStateMachineTypeArguments(MethodInvocation node) {
|
||||||
|
Expression receiver = node.getExpression();
|
||||||
|
if (receiver == null) return new String[]{null, null};
|
||||||
|
|
||||||
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding smBinding = findStateMachineSupertype(binding, new java.util.HashSet<>());
|
||||||
|
if (smBinding != null) {
|
||||||
|
ITypeBinding[] typeArgs = smBinding.getTypeArguments();
|
||||||
|
if (typeArgs.length >= 2) {
|
||||||
|
return new String[]{typeArgs[0].getQualifiedName(), typeArgs[1].getQualifiedName()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: search enclosing class/method for variable declaration
|
||||||
|
Type declType = findReceiverTypeManually(receiver, node);
|
||||||
|
if (declType instanceof ParameterizedType pt) {
|
||||||
|
List<?> typeArgs = pt.typeArguments();
|
||||||
|
if (typeArgs.size() >= 2) {
|
||||||
|
CompilationUnit cu = (CompilationUnit) node.getRoot();
|
||||||
|
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), cu);
|
||||||
|
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), cu);
|
||||||
|
return new String[]{stateType, eventType};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ITypeBinding findStateMachineSupertype(ITypeBinding binding, java.util.Set<String> visited) {
|
||||||
|
ITypeBinding current = binding;
|
||||||
|
while (current != null) {
|
||||||
|
String erasureName = current.getErasure().getQualifiedName();
|
||||||
|
if (erasureName != null && !visited.add(erasureName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ("org.springframework.statemachine.StateMachine".equals(erasureName)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
for (ITypeBinding iface : current.getInterfaces()) {
|
||||||
|
ITypeBinding res = findStateMachineSupertype(iface, visited);
|
||||||
|
if (res != null) return res;
|
||||||
|
}
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Type findReceiverTypeManually(Expression receiver, MethodInvocation node) {
|
||||||
|
if (!(receiver instanceof SimpleName sn)) return null;
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(node);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
// Check params
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return svd.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check local variables
|
||||||
|
if (enclosingMethod.getBody() != null) {
|
||||||
|
Type[] found = new Type[1];
|
||||||
|
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragObj : vds.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(varName)) {
|
||||||
|
found[0] = vds.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(vds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (found[0] != null) return found[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check fields
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(node);
|
||||||
|
if (enclosingType != null) {
|
||||||
|
for (FieldDeclaration fd : enclosingType.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(varName)) {
|
||||||
|
return fd.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTypeToFqn(Type type, CompilationUnit cu) {
|
||||||
|
if (type == null) return null;
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((ParameterizedType) type).getType(), cu);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to import matching
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check package name
|
||||||
|
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
|
if (!packageName.isEmpty()) {
|
||||||
|
AbstractTypeDeclaration localTd = context.getAbstractTypeDeclaration(packageName + "." + simpleName);
|
||||||
|
if (localTd != null) return context.getFqn(localTd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||||
List<String> args = resolveArguments(node.arguments());
|
List<String> args = resolveArguments(node.arguments());
|
||||||
for (String calledMethod : calledMethods) {
|
for (String calledMethod : calledMethods) {
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
|
||||||
}
|
}
|
||||||
for (Object argObj : node.arguments()) {
|
for (Object argObj : node.arguments()) {
|
||||||
if (argObj instanceof ExpressionMethodReference emr) {
|
if (argObj instanceof ExpressionMethodReference emr) {
|
||||||
@@ -45,7 +46,14 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
if (td2 != null) {
|
if (td2 != null) {
|
||||||
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||||
if (refMethod != null) {
|
if (refMethod != null) {
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
List<String> implicitArgs = new ArrayList<>();
|
||||||
|
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||||
|
implicitArgs.add(node.getExpression().toString());
|
||||||
|
} else {
|
||||||
|
implicitArgs.addAll(args);
|
||||||
|
}
|
||||||
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs, receiver));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -58,11 +66,18 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
if (fallbackTypeFqn != null) {
|
if (fallbackTypeFqn != null) {
|
||||||
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
List<String> implicitArgs = new ArrayList<>();
|
||||||
|
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||||
|
implicitArgs.add(node.getExpression().toString());
|
||||||
|
} else {
|
||||||
|
implicitArgs.addAll(args);
|
||||||
|
}
|
||||||
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs, receiver));
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||||
for (String impl : impls) {
|
for (String impl : impls) {
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +109,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
calledMethod = superFqn + "." + methodName;
|
calledMethod = superFqn + "." + methodName;
|
||||||
}
|
}
|
||||||
List<String> args = resolveArguments(node.arguments());
|
List<String> args = resolveArguments(node.arguments());
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
String receiver = "super";
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,62 +122,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<String> resolveArguments(List<?> astArguments) {
|
@Override
|
||||||
List<String> args = new ArrayList<>();
|
protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
|
||||||
for (Object argObj : astArguments) {
|
// Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
|
||||||
Expression expr = (Expression) argObj;
|
// where we can substitute the actual receiver from the call edge
|
||||||
|
boolean isThisOrSuperGetter = (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi
|
||||||
|
&& (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression || mi.getExpression() instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation)
|
||||||
|
&& mi.arguments().isEmpty()) || (originalExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi && smi.arguments().isEmpty());
|
||||||
|
|
||||||
// Extract from lambda
|
if (isThisOrSuperGetter) {
|
||||||
if (expr instanceof LambdaExpression le) {
|
return originalExpr.toString(); // Preserve "this.getTransitionType()" or "super.getEvent()" for later substitution
|
||||||
ASTNode body = le.getBody();
|
|
||||||
if (body instanceof Expression bodyExpr) {
|
|
||||||
expr = bodyExpr;
|
|
||||||
} else if (body instanceof Block block) {
|
|
||||||
for (Object stmtObj : block.statements()) {
|
|
||||||
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
|
||||||
expr = rs.getExpression();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expr instanceof ExpressionMethodReference emr) {
|
|
||||||
TypeDeclaration td = findEnclosingType(expr);
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
|
|
||||||
if (md != null && md.getBody() != null) {
|
|
||||||
for (Object stmtObj : md.getBody().statements()) {
|
|
||||||
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
|
||||||
expr = rs.getExpression();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
|
||||||
Expression tracedExpr = traceVariable(expr);
|
|
||||||
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
|
||||||
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
|
|
||||||
Expression firstArg = (Expression) cic.arguments().get(0);
|
|
||||||
String resolved = constantResolver.resolve(firstArg, context);
|
|
||||||
if (resolved != null) {
|
|
||||||
expr = firstArg; // Only unwrap if it's actually a constant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String val = constantResolver.resolve(expr, context);
|
|
||||||
if (val == null) {
|
|
||||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
|
||||||
}
|
|
||||||
args.add(val);
|
|
||||||
}
|
}
|
||||||
return args;
|
|
||||||
|
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized
|
||||||
|
// from a ClassInstanceCreation (directly or via a factory method). This avoids
|
||||||
|
// the broad ENUM_SET fallback when the getter transforms one enum to another.
|
||||||
|
if (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation getterMi
|
||||||
|
&& getterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName instanceSn
|
||||||
|
&& getterMi.arguments().isEmpty()) {
|
||||||
|
String narrowed = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
|
||||||
|
if (narrowed != null) {
|
||||||
|
return narrowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||||
@@ -175,6 +160,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
String definingClass = findDefiningClass(className, methodName);
|
||||||
|
if (definingClass != null && !definingClass.equals(className)) {
|
||||||
|
allResolved.set(0, definingClass + "." + methodName);
|
||||||
|
className = definingClass;
|
||||||
|
}
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(className);
|
List<String> impls = context.getImplementations(className);
|
||||||
for (String impl : impls) {
|
for (String impl : impls) {
|
||||||
allResolved.add(impl + "." + methodName);
|
allResolved.add(impl + "." + methodName);
|
||||||
@@ -184,14 +175,32 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return allResolved;
|
return allResolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String findDefiningClass(String className, String methodName) {
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) return null;
|
||||||
|
|
||||||
|
String resolvedMethodFqn = resolveMethodInTypeHierarchy(td, methodName);
|
||||||
|
if (resolvedMethodFqn != null) {
|
||||||
|
return resolvedMethodFqn.substring(0, resolvedMethodFqn.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected String resolveCalledMethod(MethodInvocation node) {
|
protected String resolveCalledMethod(MethodInvocation node) {
|
||||||
Expression receiver = node.getExpression();
|
Expression receiver = node.getExpression();
|
||||||
String methodName = node.getName().getIdentifier();
|
String methodName = node.getName().getIdentifier();
|
||||||
|
|
||||||
|
if (receiver instanceof MethodInvocation miReceiver) {
|
||||||
|
String returnType = resolveMethodInvocationReturnType(miReceiver);
|
||||||
|
if (returnType != null) {
|
||||||
|
return returnType + "." + methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (receiver == null) {
|
if (receiver == null) {
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
return resolveMethodInType(td, methodName);
|
return resolveMethodInTypeHierarchy(td, methodName);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -217,12 +226,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return receiverName + "." + methodName;
|
return receiverName + "." + methodName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (receiver instanceof QualifiedName qn) {
|
||||||
|
String fqn = qn.getFullyQualifiedName();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(fqn);
|
||||||
|
if (td != null) {
|
||||||
|
return fqn + "." + methodName;
|
||||||
|
}
|
||||||
|
return fqn + "." + methodName;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
protected String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
||||||
String varName = receiverNameNode.getIdentifier();
|
String varName = receiverNameNode.getIdentifier();
|
||||||
|
|
||||||
|
CompilationUnit cu = receiverNameNode.getRoot() instanceof CompilationUnit ? (CompilationUnit) receiverNameNode.getRoot() : null;
|
||||||
|
TypeDeclaration staticTd = context.getTypeDeclaration(varName, cu);
|
||||||
|
if (staticTd != null) {
|
||||||
|
return context.getFqn(staticTd);
|
||||||
|
}
|
||||||
|
TypeDeclaration fallbackStaticTd = context.getTypeDeclaration(varName);
|
||||||
|
if (fallbackStaticTd != null) {
|
||||||
|
return context.getFqn(fallbackStaticTd);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Check local variables in enclosing method
|
// 1. Check local variables in enclosing method
|
||||||
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||||
if (enclosingMethod != null) {
|
if (enclosingMethod != null) {
|
||||||
@@ -254,10 +282,11 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check fields in enclosing class
|
// 2. Check fields in enclosing class and its superclasses
|
||||||
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||||
if (enclosingType != null) {
|
TypeDeclaration currentType = enclosingType;
|
||||||
for (FieldDeclaration field : enclosingType.getFields()) {
|
while (currentType != null) {
|
||||||
|
for (FieldDeclaration field : currentType.getFields()) {
|
||||||
for (Object fragObj : field.fragments()) {
|
for (Object fragObj : field.fragments()) {
|
||||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
if (frag.getName().getIdentifier().equals(varName)) {
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
@@ -265,6 +294,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
String superclass = context.getSuperclassFqn(currentType);
|
||||||
|
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -299,13 +330,299 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return simpleName;
|
return simpleName;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String resolveMethodInType(TypeDeclaration td, String methodName) {
|
/**
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
* Attempts to narrow the resolution of {@code instanceVar.getterMethod()} when the
|
||||||
if (md != null) {
|
* instance variable is initialised from a {@link org.eclipse.jdt.core.dom.ClassInstanceCreation}
|
||||||
TypeDeclaration declaringTd = findEnclosingType(md);
|
* (either directly or via a factory/mapper method).
|
||||||
return context.getFqn(declaringTd) + "." + methodName;
|
*
|
||||||
|
* <p>This handles both:
|
||||||
|
* <ul>
|
||||||
|
* <li>Simple pass-through: {@code Wrapper(MyEnum e){this.e=e;} getE(){return e;}}</li>
|
||||||
|
* <li>Transformation: {@code getSmEvent(){return switch(dtoEnum){case A->SmEvents.X;…};}}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return the resolved constant string, or {@code null} when narrowing is not possible
|
||||||
|
*/
|
||||||
|
private String tryNarrowGetterViaLocalVarChain(
|
||||||
|
MethodInvocation getterCall, SimpleName instanceSn) {
|
||||||
|
|
||||||
|
MethodDeclaration enclosing = findEnclosingMethod(getterCall);
|
||||||
|
if (enclosing == null || enclosing.getBody() == null) return null;
|
||||||
|
|
||||||
|
String varName = instanceSn.getIdentifier();
|
||||||
|
String getterName = getterCall.getName().getIdentifier();
|
||||||
|
|
||||||
|
// Find the declared type and initialiser of the local variable
|
||||||
|
String[] varTypeName = {null};
|
||||||
|
Expression[] initExpr = {null};
|
||||||
|
enclosing.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragObj : vds.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
varTypeName[0] = vds.getType().toString();
|
||||||
|
initExpr[0] = frag.getInitializer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(vds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (varTypeName[0] == null) return null;
|
||||||
|
|
||||||
|
// Obtain the ClassInstanceCreation that created the object
|
||||||
|
org.eclipse.jdt.core.dom.ClassInstanceCreation cic = null;
|
||||||
|
String receiverType = null;
|
||||||
|
if (initExpr[0] instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation directCic) {
|
||||||
|
cic = directCic;
|
||||||
|
} else if (initExpr[0] instanceof MethodInvocation factoryMi
|
||||||
|
&& (factoryMi.getExpression() instanceof SimpleName || factoryMi.getExpression() instanceof QualifiedName)) {
|
||||||
|
// e.g. "mapper.toMsg(dto)" or "com.example.Factory.toMsg()"
|
||||||
|
if (factoryMi.getExpression() instanceof SimpleName receiverSn) {
|
||||||
|
receiverType = resolveReceiverTypeFallback(receiverSn);
|
||||||
|
} else if (factoryMi.getExpression() instanceof QualifiedName qn) {
|
||||||
|
receiverType = qn.getFullyQualifiedName();
|
||||||
|
}
|
||||||
|
if (receiverType != null) {
|
||||||
|
cic = findReturnedCIC(receiverType, factoryMi.getName().getIdentifier(), context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cic == null) {
|
||||||
|
// Support builder pattern: e.g. EventWrapper.builder().event(TransitionEnum.STATE_X).build()
|
||||||
|
if (initExpr[0] instanceof MethodInvocation initMi) {
|
||||||
|
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||||
|
Expression currentMi = initMi;
|
||||||
|
while (currentMi instanceof MethodInvocation chainMi) {
|
||||||
|
String mName = chainMi.getName().getIdentifier();
|
||||||
|
if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
|
||||||
|
if (!chainMi.arguments().isEmpty()) {
|
||||||
|
return chainMi.arguments().get(0).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentMi = chainMi.getExpression();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the variable's declared type to a TypeDeclaration
|
||||||
|
CompilationUnit cu = (CompilationUnit) getterCall.getRoot();
|
||||||
|
TypeDeclaration varTypeTd = context.getTypeDeclaration(varTypeName[0], cu);
|
||||||
|
if (varTypeTd == null) varTypeTd = context.getTypeDeclaration(varTypeName[0]);
|
||||||
|
if (varTypeTd == null) return null;
|
||||||
|
|
||||||
|
// Evaluate the getter body with field values substituted from the CIC's constructor args
|
||||||
|
java.util.Map<String, String> initialLocals = new java.util.HashMap<>();
|
||||||
|
if (initExpr[0] instanceof MethodInvocation factoryMi) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(context.getTypeDeclaration(receiverType != null ? receiverType : varTypeName[0]), factoryMi.getName().getIdentifier(), true);
|
||||||
|
if (md != null) {
|
||||||
|
for (int i = 0; i < md.parameters().size() && i < factoryMi.arguments().size(); i++) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
|
||||||
|
Expression arg = (Expression) factoryMi.arguments().get(i);
|
||||||
|
String resolvedVal = constantResolver.resolve(arg, context);
|
||||||
|
if (resolvedVal != null) {
|
||||||
|
initialLocals.put(param.getName().getIdentifier(), resolvedVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Map<String, String> fieldValues = constructorAnalyzer.buildFieldValuesFromCIC(varTypeTd, cic, context, this::evaluateMethodCallInConstructor, initialLocals);
|
||||||
|
if (fieldValues.isEmpty()) return null;
|
||||||
|
|
||||||
|
// Track setter calls on the variable between its initialization and the getter call
|
||||||
|
// to capture field updates like e.setType("NEW_VALUE")
|
||||||
|
trackSetterCallsBetween(enclosing.getBody(), varName, getterCall, fieldValues, context);
|
||||||
|
|
||||||
|
MethodDeclaration getter = context.findMethodDeclaration(varTypeTd, getterName, true);
|
||||||
|
if (getter == null || getter.getBody() == null) return null;
|
||||||
|
|
||||||
|
return constantResolver.evaluateMethodBodyWithLocals(
|
||||||
|
getter, fieldValues, context, new java.util.HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the method body for setter calls or field assignments to the given variable
|
||||||
|
* that occur between the variable's initialization and the getter call.
|
||||||
|
* Updates fieldValues with any field values set via setters.
|
||||||
|
*/
|
||||||
|
private void trackSetterCallsBetween(org.eclipse.jdt.core.dom.Block methodBody, String varName,
|
||||||
|
org.eclipse.jdt.core.dom.MethodInvocation getterCall,
|
||||||
|
java.util.Map<String, String> fieldValues,
|
||||||
|
CodebaseContext context) {
|
||||||
|
java.util.List<org.eclipse.jdt.core.dom.ASTNode> statements = new java.util.ArrayList<>();
|
||||||
|
for (Object stmtObj : methodBody.statements()) {
|
||||||
|
statements.add((org.eclipse.jdt.core.dom.ASTNode) stmtObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
int varDeclIndex = -1;
|
||||||
|
int getterCallIndex = -1;
|
||||||
|
for (int i = 0; i < statements.size(); i++) {
|
||||||
|
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
|
||||||
|
// Find variable declaration
|
||||||
|
if (varDeclIndex == -1) {
|
||||||
|
if (stmt instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragObj : vds.fragments()) {
|
||||||
|
if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment frag
|
||||||
|
&& frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
varDeclIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Find getter call
|
||||||
|
if (getterCallIndex == -1) {
|
||||||
|
if (stmt.toString().contains(getterCall.toString())) {
|
||||||
|
getterCallIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (varDeclIndex >= 0 && getterCallIndex > varDeclIndex) {
|
||||||
|
// Scan statements between var declaration and getter call
|
||||||
|
for (int i = varDeclIndex + 1; i < getterCallIndex; i++) {
|
||||||
|
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
|
||||||
|
if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
||||||
|
org.eclipse.jdt.core.dom.Expression expr = es.getExpression();
|
||||||
|
// Check for setter calls: var.setField(value)
|
||||||
|
if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
|
||||||
|
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
|
||||||
|
&& sn.getIdentifier().equals(varName)) {
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
// Check if it's a setter (setXxx or xxx)
|
||||||
|
if (methodName.startsWith("set") && methodName.length() > 3) {
|
||||||
|
String fieldName = Character.toLowerCase(methodName.charAt(3))
|
||||||
|
+ methodName.substring(4);
|
||||||
|
if (!mi.arguments().isEmpty()) {
|
||||||
|
org.eclipse.jdt.core.dom.Expression arg = (org.eclipse.jdt.core.dom.Expression) mi.arguments().get(0);
|
||||||
|
String val = constantResolver.resolve(arg, context);
|
||||||
|
if (val != null) {
|
||||||
|
fieldValues.put(fieldName, val);
|
||||||
|
fieldValues.put("this." + fieldName, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for direct field assignments: var.field = value
|
||||||
|
else if (expr instanceof org.eclipse.jdt.core.dom.Assignment assignment) {
|
||||||
|
if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
|
||||||
|
if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
|
||||||
|
&& sn.getIdentifier().equals(varName)) {
|
||||||
|
String fieldName = fa.getName().getIdentifier();
|
||||||
|
org.eclipse.jdt.core.dom.Expression rhs = assignment.getRightHandSide();
|
||||||
|
String val = constantResolver.resolve(rhs, context);
|
||||||
|
if (val != null) {
|
||||||
|
fieldValues.put(fieldName, val);
|
||||||
|
fieldValues.put("this." + fieldName, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a string representation of the receiver expression from a MethodInvocation.
|
||||||
|
* Handles ThisExpression, SimpleName, and other expression types.
|
||||||
|
*/
|
||||||
|
protected String getReceiverString(Expression receiver) {
|
||||||
|
if (receiver == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (receiver instanceof ThisExpression) {
|
||||||
|
return "this";
|
||||||
|
}
|
||||||
|
if (receiver instanceof SuperMethodInvocation) {
|
||||||
|
return "super";
|
||||||
|
}
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return receiver.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the receiver expression used in the caller method when calling the target method.
|
||||||
|
* Walks the caller's AST to find a MethodInvocation with the target method name
|
||||||
|
* and returns the receiver string.
|
||||||
|
*/
|
||||||
|
protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
|
||||||
|
int lastDot = callerFqn.lastIndexOf('.');
|
||||||
|
if (lastDot < 0) return null;
|
||||||
|
String callerClass = callerFqn.substring(0, lastDot);
|
||||||
|
String callerMethod = callerFqn.substring(lastDot + 1);
|
||||||
|
|
||||||
|
int targetLastDot = targetMethodFqn.lastIndexOf('.');
|
||||||
|
if (targetLastDot < 0) return null;
|
||||||
|
String targetMethodName = targetMethodFqn.substring(targetLastDot + 1);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
|
||||||
|
if (callerTd == null) return null;
|
||||||
|
|
||||||
|
MethodDeclaration callerMd = context.findMethodDeclaration(callerTd, callerMethod, false);
|
||||||
|
if (callerMd == null || callerMd.getBody() == null) return null;
|
||||||
|
|
||||||
|
final String[] foundReceiver = {null};
|
||||||
|
callerMd.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (foundReceiver[0] != null) return false;
|
||||||
|
String called = resolveCalledMethod(node);
|
||||||
|
if (called != null && (called.equals(targetMethodFqn) || called.endsWith("." + targetMethodName))) {
|
||||||
|
foundReceiver[0] = getReceiverString(node.getExpression());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return foundReceiver[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
String receiverType = null;
|
||||||
|
if (receiver == null) {
|
||||||
|
TypeDeclaration td = findEnclosingType(mi);
|
||||||
|
if (td != null) {
|
||||||
|
receiverType = context.getFqn(td);
|
||||||
|
}
|
||||||
|
} else if (receiver instanceof SimpleName sn) {
|
||||||
|
receiverType = resolveReceiverTypeFallback(sn);
|
||||||
|
} else if (receiver instanceof FieldAccess fa) {
|
||||||
|
receiverType = resolveReceiverTypeFallback(fa.getName());
|
||||||
|
} else if (receiver instanceof MethodInvocation innerMi) {
|
||||||
|
receiverType = resolveMethodInvocationReturnType(innerMi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiverType == null) {
|
||||||
|
ITypeBinding binding = receiver != null ? receiver.resolveTypeBinding() : null;
|
||||||
|
if (binding != null) {
|
||||||
|
receiverType = binding.getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiverType != null) {
|
||||||
|
if (receiverType.contains("<")) {
|
||||||
|
receiverType = receiverType.substring(0, receiverType.indexOf('<'));
|
||||||
|
}
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(receiverType);
|
||||||
|
if (td == null && receiverType.contains(".")) {
|
||||||
|
String simpleClass = receiverType.substring(receiverType.lastIndexOf('.') + 1);
|
||||||
|
td = context.getTypeDeclaration(simpleClass);
|
||||||
|
}
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getReturnType2() != null) {
|
||||||
|
return resolveTypeToFqn(md.getReturnType2(), mi);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
if (td2 != null) {
|
if (td2 != null) {
|
||||||
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||||
if (refMethod != null) {
|
if (refMethod != null) {
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
List<String> implicitArgs = new ArrayList<>();
|
||||||
|
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||||
|
implicitArgs.add(node.getExpression().toString());
|
||||||
|
} else {
|
||||||
|
implicitArgs.addAll(args);
|
||||||
|
}
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -62,7 +68,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
if (fallbackTypeFqn != null) {
|
if (fallbackTypeFqn != null) {
|
||||||
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
List<String> implicitArgs = new ArrayList<>();
|
||||||
|
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||||
|
implicitArgs.add(node.getExpression().toString());
|
||||||
|
} else {
|
||||||
|
implicitArgs.addAll(args);
|
||||||
|
}
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||||
for (String impl : impls) {
|
for (String impl : impls) {
|
||||||
@@ -110,62 +122,28 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> resolveArguments(List<?> astArguments) {
|
@Override
|
||||||
List<String> args = new ArrayList<>();
|
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
|
||||||
for (Object argObj : astArguments) {
|
if (expr == null) {
|
||||||
Expression expr = (Expression) argObj;
|
return "null";
|
||||||
|
|
||||||
// Extract from lambda
|
|
||||||
if (expr instanceof LambdaExpression le) {
|
|
||||||
ASTNode body = le.getBody();
|
|
||||||
if (body instanceof Expression bodyExpr) {
|
|
||||||
expr = bodyExpr;
|
|
||||||
} else if (body instanceof Block block) {
|
|
||||||
for (Object stmtObj : block.statements()) {
|
|
||||||
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
|
||||||
expr = rs.getExpression();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expr instanceof ExpressionMethodReference emr) {
|
|
||||||
TypeDeclaration td = findEnclosingType(expr);
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
|
|
||||||
if (md != null && md.getBody() != null) {
|
|
||||||
for (Object stmtObj : md.getBody().statements()) {
|
|
||||||
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
|
||||||
expr = rs.getExpression();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
|
||||||
Expression tracedExpr = traceVariable(expr);
|
|
||||||
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
|
||||||
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
|
|
||||||
Expression firstArg = (Expression) cic.arguments().get(0);
|
|
||||||
String resolved = constantResolver.resolve(firstArg, context);
|
|
||||||
if (resolved != null) {
|
|
||||||
expr = firstArg; // Only unwrap if it's actually a constant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String val = constantResolver.resolve(expr, context);
|
|
||||||
if (val == null) {
|
|
||||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
|
||||||
}
|
|
||||||
args.add(val);
|
|
||||||
}
|
}
|
||||||
return args;
|
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||||
|
org.eclipse.jdt.core.dom.Expression unwrappedBuilder = unwrapMessageBuilder(expr);
|
||||||
|
if (unwrappedBuilder != expr) {
|
||||||
|
expr = unwrappedBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace variable to resolve local variable references
|
||||||
|
org.eclipse.jdt.core.dom.Expression tracedExpr = traceVariable(expr);
|
||||||
|
if (tracedExpr instanceof org.eclipse.jdt.core.dom.QualifiedName
|
||||||
|
|| tracedExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation
|
||||||
|
|| tracedExpr instanceof org.eclipse.jdt.core.dom.StringLiteral
|
||||||
|
|| tracedExpr instanceof org.eclipse.jdt.core.dom.NumberLiteral) {
|
||||||
|
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to base class for lambda, method reference, and constructor unwrapping
|
||||||
|
return super.resolveArgument(expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||||
@@ -192,6 +170,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
Expression receiver = node.getExpression();
|
Expression receiver = node.getExpression();
|
||||||
String methodName = node.getName().getIdentifier();
|
String methodName = node.getName().getIdentifier();
|
||||||
|
|
||||||
|
if (receiver instanceof MethodInvocation miReceiver) {
|
||||||
|
String returnType = resolveMethodInvocationReturnType(miReceiver);
|
||||||
|
if (returnType != null) {
|
||||||
|
return returnType + "." + methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (receiver == null) {
|
if (receiver == null) {
|
||||||
TypeDeclaration td = findEnclosingType(node);
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
@@ -303,10 +288,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check fields in enclosing class
|
// 2. Check fields in enclosing class and its superclasses
|
||||||
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||||
if (enclosingType != null) {
|
TypeDeclaration currentType = enclosingType;
|
||||||
for (FieldDeclaration field : enclosingType.getFields()) {
|
while (currentType != null) {
|
||||||
|
for (FieldDeclaration field : currentType.getFields()) {
|
||||||
for (Object fragObj : field.fragments()) {
|
for (Object fragObj : field.fragments()) {
|
||||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
if (frag.getName().getIdentifier().equals(varName)) {
|
if (frag.getName().getIdentifier().equals(varName)) {
|
||||||
@@ -314,6 +300,8 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
String superclass = context.getSuperclassFqn(currentType);
|
||||||
|
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -348,13 +336,65 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return simpleName;
|
return simpleName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveMethodInType(TypeDeclaration td, String methodName) {
|
private Expression unwrapMessageBuilder(Expression expr) {
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
if (expr instanceof MethodInvocation mi) {
|
||||||
if (md != null) {
|
MethodInvocation current = mi;
|
||||||
TypeDeclaration declaringTd = findEnclosingType(md);
|
while (current != null) {
|
||||||
return context.getFqn(declaringTd) + "." + methodName;
|
String name = current.getName().getIdentifier();
|
||||||
|
if (("withPayload".equals(name) || "just".equals(name)) && !current.arguments().isEmpty()) {
|
||||||
|
return (Expression) current.arguments().get(0);
|
||||||
|
}
|
||||||
|
Expression receiver = current.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation nextMi) {
|
||||||
|
current = nextMi;
|
||||||
|
} else {
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
String receiverType = null;
|
||||||
|
if (receiver == null) {
|
||||||
|
TypeDeclaration td = findEnclosingType(mi);
|
||||||
|
if (td != null) {
|
||||||
|
receiverType = context.getFqn(td);
|
||||||
|
}
|
||||||
|
} else if (receiver instanceof SimpleName sn) {
|
||||||
|
receiverType = resolveReceiverTypeFallback(sn);
|
||||||
|
} else if (receiver instanceof FieldAccess fa) {
|
||||||
|
receiverType = resolveReceiverTypeFallback(fa.getName());
|
||||||
|
} else if (receiver instanceof MethodInvocation innerMi) {
|
||||||
|
receiverType = resolveMethodInvocationReturnType(innerMi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiverType == null) {
|
||||||
|
ITypeBinding binding = receiver != null ? receiver.resolveTypeBinding() : null;
|
||||||
|
if (binding != null) {
|
||||||
|
receiverType = binding.getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiverType != null) {
|
||||||
|
if (receiverType.contains("<")) {
|
||||||
|
receiverType = receiverType.substring(0, receiverType.indexOf('<'));
|
||||||
|
}
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(receiverType);
|
||||||
|
if (td == null && receiverType.contains(".")) {
|
||||||
|
String simpleClass = receiverType.substring(receiverType.lastIndexOf('.') + 1);
|
||||||
|
td = context.getTypeDeclaration(simpleClass);
|
||||||
|
}
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getReturnType2() != null) {
|
||||||
|
return resolveTypeToFqn(md.getReturnType2(), mi);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class TypeResolver {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
public TypeResolver(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getParameterIndex(String methodFqn, String paramName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return -1;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null) {
|
||||||
|
for (int i = 0; i < md.parameters().size(); i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
|
||||||
|
if (svd.getName().getIdentifier().equals(paramName)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getParameterType(String methodFqn, int index) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && index < md.parameters().size()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(index);
|
||||||
|
return svd.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTypeCompatible(String actualType, String expectedType) {
|
||||||
|
if (actualType == null || expectedType == null) return true;
|
||||||
|
if (expectedType.equals("Object") || expectedType.equals("java.lang.Object")) return true;
|
||||||
|
if (actualType.equals("Object") || actualType.equals("java.lang.Object")) return true;
|
||||||
|
if (expectedType.length() == 1 && Character.isUpperCase(expectedType.charAt(0))) return true;
|
||||||
|
if (actualType.length() == 1 && Character.isUpperCase(actualType.charAt(0))) return true;
|
||||||
|
|
||||||
|
String originalExpectedType = expectedType;
|
||||||
|
|
||||||
|
if (actualType.contains("<")) {
|
||||||
|
String rawType = actualType.substring(0, actualType.indexOf('<'));
|
||||||
|
if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) {
|
||||||
|
int start = actualType.indexOf('<');
|
||||||
|
int end = actualType.lastIndexOf('>');
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
actualType = actualType.substring(start + 1, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expectedType.contains("<")) {
|
||||||
|
String rawType = expectedType.substring(0, expectedType.indexOf('<'));
|
||||||
|
if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) {
|
||||||
|
int start = expectedType.indexOf('<');
|
||||||
|
int end = expectedType.lastIndexOf('>');
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
expectedType = expectedType.substring(start + 1, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualType.contains("<")) {
|
||||||
|
actualType = actualType.substring(0, actualType.indexOf('<'));
|
||||||
|
}
|
||||||
|
if (expectedType.contains("<")) {
|
||||||
|
expectedType = expectedType.substring(0, expectedType.indexOf('<'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualType.equals(expectedType)) return true;
|
||||||
|
if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true;
|
||||||
|
|
||||||
|
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType);
|
||||||
|
if (td != null) {
|
||||||
|
if (td instanceof TypeDeclaration typeDecl) {
|
||||||
|
String superFqn = context.getSuperclassFqn(typeDecl);
|
||||||
|
while (superFqn != null) {
|
||||||
|
if (superFqn.contains("<")) {
|
||||||
|
superFqn = superFqn.substring(0, superFqn.indexOf('<'));
|
||||||
|
}
|
||||||
|
if (superFqn.equals(expectedType) || superFqn.endsWith("." + expectedType) || expectedType.endsWith("." + superFqn)) return true;
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
superFqn = superTd != null ? context.getSuperclassFqn(superTd) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List interfaces = java.util.Collections.emptyList();
|
||||||
|
if (td instanceof TypeDeclaration typeDecl) {
|
||||||
|
interfaces = typeDecl.superInterfaceTypes();
|
||||||
|
} else if (td instanceof EnumDeclaration enumDecl) {
|
||||||
|
interfaces = enumDecl.superInterfaceTypes();
|
||||||
|
} else if (td instanceof RecordDeclaration recordDecl) {
|
||||||
|
interfaces = recordDecl.superInterfaceTypes();
|
||||||
|
}
|
||||||
|
for (Object interfaceType : interfaces) {
|
||||||
|
String itStr = interfaceType.toString();
|
||||||
|
if (itStr.contains("<")) {
|
||||||
|
itStr = itStr.substring(0, itStr.indexOf('<'));
|
||||||
|
}
|
||||||
|
if (itStr.equals(expectedType) || itStr.endsWith("." + expectedType) || expectedType.endsWith("." + itStr)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalExpectedType.contains("<")) {
|
||||||
|
int start = originalExpectedType.indexOf('<');
|
||||||
|
int end = originalExpectedType.lastIndexOf('>');
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
String sub = originalExpectedType.substring(start + 1, end);
|
||||||
|
for (String part : sub.split(",")) {
|
||||||
|
String typeArg = part.trim();
|
||||||
|
if (isTypeCompatible(actualType, typeArg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,623 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class VariableTracer {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
private ConstantExtractor constantExtractor;
|
||||||
|
|
||||||
|
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
|
||||||
|
|
||||||
|
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
|
||||||
|
this.context = context;
|
||||||
|
this.constantResolver = constantResolver;
|
||||||
|
this.dataFlowModel = new click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstantExtractor(ConstantExtractor constantExtractor) {
|
||||||
|
this.constantExtractor = constantExtractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Expression traceVariable(Expression expr) {
|
||||||
|
List<Expression> all = traceVariableAll(expr);
|
||||||
|
return all.isEmpty() ? expr : all.get(all.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Expression> traceVariableAll(Expression expr) {
|
||||||
|
return dataFlowModel.getReachingDefinitions(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
|
||||||
|
return dataFlowModel.getReachingDefinitions(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Expression traceLocalSetter(String methodFqn, String varName, String getterName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
final Expression[] setterArg = new Expression[1];
|
||||||
|
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (node.getName().getIdentifier().equalsIgnoreCase("set" + propName) || node.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
if (node.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
|
||||||
|
if (!node.arguments().isEmpty()) {
|
||||||
|
setterArg[0] = (Expression) node.arguments().get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof FieldAccess fa) {
|
||||||
|
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
||||||
|
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
setterArg[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.getLeftHandSide() instanceof QualifiedName qqn) {
|
||||||
|
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
||||||
|
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
setterArg[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return setterArg[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
|
||||||
|
if (methodFqn == null) return null;
|
||||||
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) {
|
||||||
|
String fqn = methodFqn.substring(0, lastDot);
|
||||||
|
String methodName = methodFqn.substring(lastDot + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(fqn);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
|
||||||
|
if (md != null) {
|
||||||
|
for (Object pObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(cleanFieldName)) {
|
||||||
|
return svd.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String[] foundType = new String[1];
|
||||||
|
if (md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
|
||||||
|
foundType[0] = node.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(LambdaExpression node) {
|
||||||
|
for (Object paramObj : node.parameters()) {
|
||||||
|
VariableDeclaration param = (VariableDeclaration) paramObj;
|
||||||
|
if (param.getName().getIdentifier().equals(cleanFieldName)) {
|
||||||
|
if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
|
||||||
|
foundType[0] = svd.getType().toString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
if (parent instanceof MethodInvocation mi) {
|
||||||
|
Expression caller = mi.getExpression();
|
||||||
|
|
||||||
|
boolean hasMap = false;
|
||||||
|
while (caller instanceof MethodInvocation chainMi) {
|
||||||
|
String mName = chainMi.getName().getIdentifier();
|
||||||
|
if (mName.equals("map") || mName.equals("flatMap")) {
|
||||||
|
hasMap = true;
|
||||||
|
if (chainMi.arguments().size() == 1 && chainMi.arguments().get(0) instanceof LambdaExpression lambda) {
|
||||||
|
if (lambda.getBody() instanceof MethodInvocation mapMi) {
|
||||||
|
Expression mapCaller = mapMi.getExpression();
|
||||||
|
if (mapCaller instanceof SimpleName mapSn) {
|
||||||
|
String mapCallerType = getVariableDeclaredType(methodFqn, mapSn.getIdentifier());
|
||||||
|
if (mapCallerType != null) {
|
||||||
|
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||||
|
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(mapCallerType, cu);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration mapMd = context.findMethodDeclaration(td, mapMi.getName().getIdentifier(), true);
|
||||||
|
if (mapMd != null && mapMd.getReturnType2() != null) {
|
||||||
|
foundType[0] = mapMd.getReturnType2().toString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mName.equals("stream") || mName.equals("filter") || mName.equals("map") ||
|
||||||
|
mName.equals("flatMap") || mName.equals("peek") || mName.equals("distinct") ||
|
||||||
|
mName.equals("sorted") || mName.equals("limit") || mName.equals("skip")) {
|
||||||
|
caller = chainMi.getExpression();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMap) {
|
||||||
|
if (caller instanceof SimpleName callerSn) {
|
||||||
|
String callerName = callerSn.getIdentifier();
|
||||||
|
if (!callerName.equals(cleanFieldName)) {
|
||||||
|
String callerType = getVariableDeclaredType(methodFqn, callerName);
|
||||||
|
if (callerType != null && callerType.contains("<")) {
|
||||||
|
int start = callerType.indexOf('<');
|
||||||
|
int end = callerType.lastIndexOf('>');
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
foundType[0] = callerType.substring(start + 1, end);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (caller instanceof FieldAccess fa) {
|
||||||
|
String callerName = fa.getName().getIdentifier();
|
||||||
|
if (!callerName.equals(cleanFieldName)) {
|
||||||
|
String callerType = getVariableDeclaredType(methodFqn, callerName);
|
||||||
|
if (callerType != null && callerType.contains("<")) {
|
||||||
|
int start = callerType.indexOf('<');
|
||||||
|
int end = callerType.lastIndexOf('>');
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
foundType[0] = callerType.substring(start + 1, end);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (foundType[0] != null) return foundType[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to fields
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
|
||||||
|
return fd.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback: ask constantExtractor to resolve it via method return constant
|
||||||
|
if (constantExtractor != null && methodFqn.contains(".")) {
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
CompilationUnit cu = td != null && td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
List<String> values = constantExtractor.resolveMethodReturnConstant(cleanFieldName, methodName, 0, new HashSet<>(), cu);
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
// If it resolves to some class constant, we could infer type or return a fallback
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String traceLocalVariable(String methodFqn, String varName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
List<Expression> initializers = new ArrayList<>();
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
initializers.add(node.getInitializer());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
initializers.add(node.getRightHandSide());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!initializers.isEmpty()) {
|
||||||
|
List<String> stringified = new ArrayList<>();
|
||||||
|
for (Expression expr : initializers) {
|
||||||
|
Expression traced = traceVariable(expr);
|
||||||
|
if (traced instanceof MethodInvocation mi) {
|
||||||
|
Expression innerMost = unwrapMethodInvocation(mi, 0, methodFqn);
|
||||||
|
stringified.add(innerMost.toString());
|
||||||
|
} else if (traced instanceof ArrayInitializer) {
|
||||||
|
stringified.add("new Object[]" + traced.toString());
|
||||||
|
} else {
|
||||||
|
stringified.add(traced.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stringified.size() == 1) return stringified.get(0);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < stringified.size() - 1; i++) {
|
||||||
|
sb.append("true ? ").append(stringified.get(i)).append(" : ");
|
||||||
|
}
|
||||||
|
sb.append(stringified.get(stringified.size() - 1));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If local variable not found, check field assignments
|
||||||
|
String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName;
|
||||||
|
final Expression[] fieldInitializer = new Expression[1];
|
||||||
|
ASTVisitor fieldVisitor = new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) {
|
||||||
|
fieldInitializer[0] = node.getInitializer();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression left = node.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) ||
|
||||||
|
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) {
|
||||||
|
fieldInitializer[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) {
|
||||||
|
fieldInitializer[0] = frag.getInitializer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||||
|
|
||||||
|
for (MethodDeclaration classMd : td.getMethods()) {
|
||||||
|
if (classMd.isConstructor() && classMd.getBody() != null) {
|
||||||
|
classMd.getBody().accept(fieldVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||||
|
|
||||||
|
for (MethodDeclaration classMd : td.getMethods()) {
|
||||||
|
if (!classMd.isConstructor() && classMd.getBody() != null) {
|
||||||
|
classMd.getBody().accept(fieldVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String traceLocalVariable(String methodFqn, String varName, Map<String, String> parameterValues) {
|
||||||
|
String result = traceLocalVariable(methodFqn, varName);
|
||||||
|
if (result != null && parameterValues != null && !parameterValues.isEmpty()) {
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||||
|
if (td != null) {
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
final ASTNode[] switchNode = new ASTNode[1];
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
Expression init = node.getInitializer();
|
||||||
|
if (init.getNodeType() == ASTNode.SWITCH_EXPRESSION) {
|
||||||
|
switchNode[0] = init;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (init.getNodeType() == ASTNode.SWITCH_STATEMENT) {
|
||||||
|
switchNode[0] = init;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
Expression rhs = node.getRightHandSide();
|
||||||
|
if (rhs.getNodeType() == ASTNode.SWITCH_EXPRESSION) {
|
||||||
|
switchNode[0] = rhs;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rhs.getNodeType() == ASTNode.SWITCH_STATEMENT) {
|
||||||
|
switchNode[0] = rhs;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (switchNode[0] != null) {
|
||||||
|
if (switchNode[0] instanceof SwitchExpression se) {
|
||||||
|
String evaluated = constantResolver.evaluateSwitchWithParams(se, parameterValues, context);
|
||||||
|
if (evaluated != null) return evaluated;
|
||||||
|
} else if (switchNode[0] instanceof SwitchStatement ss) {
|
||||||
|
String evaluated = constantResolver.evaluateSwitchWithParams(ss, parameterValues, context);
|
||||||
|
if (evaluated != null) return evaluated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildParameterValuesMap(String caller, String target, Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
|
||||||
|
Map<String, String> paramValues = new HashMap<>();
|
||||||
|
List<CallEdge> edges = callGraph.get(caller);
|
||||||
|
if (edges == null) {
|
||||||
|
return paramValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also want to support heuristic matches using a helper if isHeuristicMatch is not directly available, but let's check
|
||||||
|
// We can check if name equals or if it resolves to target
|
||||||
|
for (CallEdge edge : edges) {
|
||||||
|
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||||
|
List<String> args = edge.getArguments();
|
||||||
|
if (args == null || args.isEmpty()) break;
|
||||||
|
|
||||||
|
int lastDot = target.lastIndexOf('.');
|
||||||
|
if (lastDot < 0) break;
|
||||||
|
String className = target.substring(0, lastDot);
|
||||||
|
String methodName = target.substring(lastDot + 1);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) break;
|
||||||
|
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md == null) break;
|
||||||
|
|
||||||
|
List<String> paramNames = new ArrayList<>();
|
||||||
|
for (Object paramObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
||||||
|
paramNames.add(param.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
int minSize = Math.min(paramNames.size(), args.size());
|
||||||
|
for (int j = 0; j < minSize; j++) {
|
||||||
|
String argValue = args.get(j);
|
||||||
|
String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph);
|
||||||
|
paramValues.put(paramNames.get(j), resolvedArgValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paramValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveArgumentValue(String argValue, String callerMethod, List<String> path, int pathIndex, Map<String, List<CallEdge>> callGraph) {
|
||||||
|
if (argValue == null) return null;
|
||||||
|
if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*")) {
|
||||||
|
return argValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int callerIndex = path.indexOf(callerMethod);
|
||||||
|
if (callerIndex <= 0) return argValue;
|
||||||
|
|
||||||
|
String prevCaller = path.get(callerIndex - 1);
|
||||||
|
List<CallEdge> prevEdges = callGraph.get(prevCaller);
|
||||||
|
if (prevEdges == null) return argValue;
|
||||||
|
|
||||||
|
for (CallEdge edge : prevEdges) {
|
||||||
|
if (edge.getTargetMethod().equals(callerMethod) || isHeuristicMatch(edge.getTargetMethod(), callerMethod)) {
|
||||||
|
List<String> prevArgs = edge.getArguments();
|
||||||
|
if (prevArgs == null || prevArgs.isEmpty()) break;
|
||||||
|
|
||||||
|
int lastDot = callerMethod.lastIndexOf('.');
|
||||||
|
if (lastDot < 0) break;
|
||||||
|
String className = callerMethod.substring(0, lastDot);
|
||||||
|
String methodName = callerMethod.substring(lastDot + 1);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) break;
|
||||||
|
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md == null) break;
|
||||||
|
|
||||||
|
List<String> paramNames = new ArrayList<>();
|
||||||
|
for (Object paramObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
||||||
|
paramNames.add(param.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
int paramIdx = paramNames.indexOf(argValue);
|
||||||
|
if (paramIdx >= 0 && paramIdx < prevArgs.size()) {
|
||||||
|
String prevArg = prevArgs.get(paramIdx);
|
||||||
|
return resolveArgumentValue(prevArg, prevCaller, path, pathIndex, callGraph);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return argValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||||
|
if (neighbor == null || target == null) return false;
|
||||||
|
if (neighbor.equals(target)) return true;
|
||||||
|
|
||||||
|
if (neighbor.contains(".") && target.contains(".")) {
|
||||||
|
String methodNeighbor = neighbor.substring(neighbor.lastIndexOf('.') + 1);
|
||||||
|
String methodTarget = target.substring(target.lastIndexOf('.') + 1);
|
||||||
|
if (!methodNeighbor.equals(methodTarget)) return false;
|
||||||
|
|
||||||
|
String classNeighbor = neighbor.substring(0, neighbor.lastIndexOf('.'));
|
||||||
|
String classTarget = target.substring(0, target.lastIndexOf('.'));
|
||||||
|
if (classNeighbor.equals(classTarget)) return true;
|
||||||
|
|
||||||
|
String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
|
||||||
|
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
|
||||||
|
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
|
||||||
|
|
||||||
|
List<String> impls = context.getImplementations(classTarget);
|
||||||
|
if (impls != null && impls.contains(classNeighbor)) return true;
|
||||||
|
|
||||||
|
List<String> implsN = context.getImplementations(classNeighbor);
|
||||||
|
if (implsN != null && implsN.contains(classTarget)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (MethodDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (TypeDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getReturnedParameterIndex(MethodDeclaration md) {
|
||||||
|
if (md.getBody() == null) return -1;
|
||||||
|
List<?> parameters = md.parameters();
|
||||||
|
if (parameters.isEmpty()) return -1;
|
||||||
|
|
||||||
|
final List<String> returnedExprs = new ArrayList<>();
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
if (node.getExpression() != null) {
|
||||||
|
returnedExprs.add(node.getExpression().toString());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (returnedExprs.size() == 1) {
|
||||||
|
String retStr = returnedExprs.get(0);
|
||||||
|
for (int i = 0; i < parameters.size(); i++) {
|
||||||
|
if (parameters.get(i) instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(retStr)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTargetMethodFqn(MethodInvocation mi, String currentMethodFqn) {
|
||||||
|
if (currentMethodFqn == null || !currentMethodFqn.contains(".")) return null;
|
||||||
|
String currentClass = currentMethodFqn.substring(0, currentMethodFqn.lastIndexOf('.'));
|
||||||
|
if (mi.getExpression() == null || mi.getExpression().toString().equals("this")) {
|
||||||
|
return currentClass + "." + mi.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
if (mi.getExpression() instanceof SimpleName sn) {
|
||||||
|
String declaredType = getVariableDeclaredType(currentMethodFqn, sn.getIdentifier());
|
||||||
|
if (declaredType != null) {
|
||||||
|
return declaredType + "." + mi.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
} else if (mi.getExpression() instanceof FieldAccess fa) {
|
||||||
|
String declaredType = getVariableDeclaredType(currentMethodFqn, fa.getName().getIdentifier());
|
||||||
|
if (declaredType != null) {
|
||||||
|
return declaredType + "." + mi.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
||||||
|
if (depth > 5) return mi;
|
||||||
|
|
||||||
|
String targetMethodFqn = resolveTargetMethodFqn(mi, methodFqn);
|
||||||
|
if (targetMethodFqn != null && targetMethodFqn.contains(".")) {
|
||||||
|
String className = targetMethodFqn.substring(0, targetMethodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = targetMethodFqn.substring(targetMethodFqn.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
TypeDeclaration currentTd = methodFqn != null && methodFqn.contains(".") ? context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))) : null;
|
||||||
|
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className, cu);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration localMd = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (localMd != null) {
|
||||||
|
int paramIdx = getReturnedParameterIndex(localMd);
|
||||||
|
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(paramIdx);
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
String exprStr = mi.getExpression().toString();
|
||||||
|
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) {
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mi.arguments().isEmpty()) {
|
||||||
|
String mName = mi.getName().getIdentifier();
|
||||||
|
String lowerName = mName.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
|
||||||
|
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
|
||||||
|
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
|
||||||
|
lowerName.startsWith("create") || lowerName.startsWith("from") ||
|
||||||
|
lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
|
||||||
|
lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
|
||||||
|
lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
|
||||||
|
lowerName.startsWith("extract") || lowerName.startsWith("get") ||
|
||||||
|
lowerName.startsWith("find") || lowerName.startsWith("validate")) {
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -361,6 +361,115 @@ public class AstTransitionParser {
|
|||||||
return mr.toString();
|
return mr.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. Bean Reference (SimpleName, FieldAccess, etc.)
|
||||||
|
String resolvedBeanBody = resolveBeanClassBody(expr, context);
|
||||||
|
if (resolvedBeanBody != null) {
|
||||||
|
return resolvedBeanBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveBeanClassBody(Expression expr, CodebaseContext context) {
|
||||||
|
String typeFqn = null;
|
||||||
|
ITypeBinding binding = expr.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
typeFqn = binding.getQualifiedName();
|
||||||
|
} else if (expr instanceof SimpleName sn) {
|
||||||
|
// Check enclosing method first for local variables
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
final String[] foundType = {null};
|
||||||
|
final Expression[] foundInitializer = {null};
|
||||||
|
enclosingMethod.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragObj : vds.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(sn.getIdentifier())) {
|
||||||
|
foundType[0] = vds.getType().toString();
|
||||||
|
foundInitializer[0] = fragment.getInitializer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(vds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundInitializer[0] != null) {
|
||||||
|
ASTNode root = expr.getRoot();
|
||||||
|
if (root instanceof CompilationUnit cu) {
|
||||||
|
String initLogic = extractInternalLogic(foundInitializer[0], cu, context);
|
||||||
|
if (initLogic != null) return initLogic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundType[0] != null) {
|
||||||
|
typeFqn = foundType[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFqn == null) {
|
||||||
|
TypeDeclaration enclosingClass = findEnclosingType(expr);
|
||||||
|
if (enclosingClass != null) {
|
||||||
|
for (FieldDeclaration fd : enclosingClass.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(sn.getIdentifier())) {
|
||||||
|
if (fragment.getInitializer() != null) {
|
||||||
|
ASTNode root = expr.getRoot();
|
||||||
|
if (root instanceof CompilationUnit cu) {
|
||||||
|
String initLogic = extractInternalLogic(fragment.getInitializer(), cu, context);
|
||||||
|
if (initLogic != null) return initLogic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typeFqn = fd.getType().toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFqn != null) {
|
||||||
|
if (typeFqn.contains("<")) {
|
||||||
|
typeFqn = typeFqn.substring(0, typeFqn.indexOf('<'));
|
||||||
|
}
|
||||||
|
TypeDeclaration beanTd = context.getTypeDeclaration(typeFqn);
|
||||||
|
if (beanTd == null) {
|
||||||
|
ASTNode root = expr.getRoot();
|
||||||
|
if (root instanceof CompilationUnit cu) {
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
if (!imp.isStatic() && !imp.isOnDemand()) {
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + typeFqn)) {
|
||||||
|
beanTd = context.getTypeDeclaration(impName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (beanTd != null) {
|
||||||
|
for (MethodDeclaration md : beanTd.getMethods()) {
|
||||||
|
String name = md.getName().getIdentifier();
|
||||||
|
if ("evaluate".equals(name) || "execute".equals(name)) {
|
||||||
|
return md.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return beanTd.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode current = node;
|
||||||
|
while (current != null) {
|
||||||
|
if (current instanceof MethodDeclaration md) return md;
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,620 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.app;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class StateMachineAdapterParser {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
private enum AnalysisGoal {TRANSITIONS, STATES}
|
||||||
|
|
||||||
|
public record AdapterStates(Set<String> initialStates, Set<String> endStates) {}
|
||||||
|
|
||||||
|
public record AdapterConfig(
|
||||||
|
List<Transition> transitions,
|
||||||
|
Set<String> initialStates,
|
||||||
|
Set<String> endStates
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public StateMachineAdapterParser(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdapterConfig parse(TypeDeclaration startClass) {
|
||||||
|
List<Transition> transitions = parseTransitions(startClass);
|
||||||
|
AdapterStates states = parseStates(startClass);
|
||||||
|
return new AdapterConfig(transitions, states.initialStates(), states.endStates());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Transition> parseTransitions(TypeDeclaration startClass) {
|
||||||
|
List<Transition> allTransitions = new ArrayList<>();
|
||||||
|
Set<MethodVisit> visitedMethods = new HashSet<>();
|
||||||
|
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
|
||||||
|
return allTransitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdapterStates parseStates(TypeDeclaration startClass) {
|
||||||
|
Set<String> initialStates = new HashSet<>();
|
||||||
|
Set<String> endStates = new HashSet<>();
|
||||||
|
Set<MethodVisit> visitedMethods = new HashSet<>();
|
||||||
|
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
|
||||||
|
return new AdapterStates(initialStates, endStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
if (currentTd == null)
|
||||||
|
return;
|
||||||
|
String fqn = context.getFqn(currentTd);
|
||||||
|
if (visitedClasses.contains(fqn))
|
||||||
|
return;
|
||||||
|
visitedClasses.add(fqn);
|
||||||
|
|
||||||
|
for (MethodDeclaration method : currentTd.getMethods()) {
|
||||||
|
if (isConfigureTransitionsMethod(method)) {
|
||||||
|
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
||||||
|
|
||||||
|
// Hierarchy - Superclass
|
||||||
|
Type superclassType = currentTd.getSuperclassType();
|
||||||
|
if (superclassType != null) {
|
||||||
|
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
||||||
|
if (superclassTd != null) {
|
||||||
|
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hierarchy - Interfaces
|
||||||
|
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
||||||
|
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||||
|
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
||||||
|
if (interfaceTd != null) {
|
||||||
|
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
||||||
|
if (visitedMethods.contains(visit)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
visitedMethods.add(visit);
|
||||||
|
|
||||||
|
Block body = method.getBody();
|
||||||
|
if (body == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
if (isTransitionChainEntry(mi)) {
|
||||||
|
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
|
||||||
|
} else if (isTransitionChainContinuation(mi, argsMap)) {
|
||||||
|
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
|
||||||
|
} else if (isForEach(mi)) {
|
||||||
|
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||||
|
} else {
|
||||||
|
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
||||||
|
Expression lambdaReceiver = mi.getExpression();
|
||||||
|
if (lambdaReceiver instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof LambdaExpression lambda) {
|
||||||
|
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||||
|
if (calledMethod == null) {
|
||||||
|
// Try to resolve method in another class if it's a static call or on a known type
|
||||||
|
Expression miReceiver = mi.getExpression();
|
||||||
|
if (miReceiver instanceof SimpleName sn) {
|
||||||
|
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
|
||||||
|
if (otherTd != null) {
|
||||||
|
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
|
||||||
|
if (calledMethod != null) {
|
||||||
|
// For calls to other classes, we switch the "searchStartClass" to that class
|
||||||
|
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||||
|
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
|
||||||
|
return transitions; // Handled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calledMethod != null) {
|
||||||
|
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||||
|
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isForEach(MethodInvocation mi) {
|
||||||
|
return mi.getName().getIdentifier().equals("forEach");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||||
|
List<?> params = lambda.parameters();
|
||||||
|
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
|
||||||
|
Object p = params.get(i);
|
||||||
|
String paramName = "";
|
||||||
|
if (p instanceof VariableDeclaration vp)
|
||||||
|
paramName = vp.getName().getIdentifier();
|
||||||
|
if (!paramName.isEmpty()) {
|
||||||
|
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||||
|
if (lambda.getBody() instanceof Block block)
|
||||||
|
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||||
|
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
|
||||||
|
} else {
|
||||||
|
if (lambda.getBody() instanceof Block block)
|
||||||
|
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||||
|
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
Expression receiver = forEachMi.getExpression();
|
||||||
|
List<Expression> elements = unrollCollection(receiver, argsMap);
|
||||||
|
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
|
||||||
|
return transitions;
|
||||||
|
|
||||||
|
Object lambdaObj = forEachMi.arguments().get(0);
|
||||||
|
if (lambdaObj instanceof LambdaExpression lambda) {
|
||||||
|
String paramName = getLambdaParamName(lambda);
|
||||||
|
for (Expression element : elements) {
|
||||||
|
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||||
|
if (!paramName.isEmpty())
|
||||||
|
lambdaArgsMap.put(paramName, element);
|
||||||
|
|
||||||
|
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||||
|
if (lambda.getBody() instanceof Block block)
|
||||||
|
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||||
|
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
|
||||||
|
} else {
|
||||||
|
if (lambda.getBody() instanceof Block block)
|
||||||
|
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||||
|
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
|
||||||
|
String paramName = efs.getParameter().getName().getIdentifier();
|
||||||
|
|
||||||
|
for (Expression element : elements) {
|
||||||
|
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
|
||||||
|
loopArgsMap.put(paramName, element);
|
||||||
|
|
||||||
|
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||||
|
if (efs.getBody() instanceof Block block)
|
||||||
|
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
|
||||||
|
else if (efs.getBody() instanceof ExpressionStatement es)
|
||||||
|
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
|
||||||
|
} else {
|
||||||
|
if (efs.getBody() instanceof Block block)
|
||||||
|
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
||||||
|
else if (efs.getBody() instanceof ExpressionStatement es)
|
||||||
|
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
|
||||||
|
Set<Expression> visited = new HashSet<>();
|
||||||
|
Expression current = expr;
|
||||||
|
while (current != null) {
|
||||||
|
if (!visited.add(current))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (current instanceof MethodInvocation mi) {
|
||||||
|
String name = mi.getName().getIdentifier();
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|
||||||
|
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
|
||||||
|
if (mi.arguments().size() == 1) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
if (arg instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof List list)
|
||||||
|
return (List<Expression>) list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (List<Expression>) mi.arguments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof List list)
|
||||||
|
return (List<Expression>) list;
|
||||||
|
if (resolved instanceof Expression e) {
|
||||||
|
current = e;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current instanceof ArrayInitializer ai)
|
||||||
|
return (List<Expression>) ai.expressions();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLambdaParamName(LambdaExpression lambda) {
|
||||||
|
if (lambda.parameters().size() == 1) {
|
||||||
|
Object p = lambda.parameters().get(0);
|
||||||
|
if (p instanceof VariableDeclaration vp)
|
||||||
|
return vp.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
for (Object stmt : block.statements()) {
|
||||||
|
if (stmt instanceof ExpressionStatement es)
|
||||||
|
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
|
||||||
|
else if (stmt instanceof VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragmentObj : vds.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
Expression initializer = fragment.getInitializer();
|
||||||
|
if (initializer != null) {
|
||||||
|
Object resolved = resolve(initializer, argsMap);
|
||||||
|
if (resolved != null)
|
||||||
|
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
||||||
|
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (stmt instanceof EnhancedForStatement efs)
|
||||||
|
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||||
|
else if (stmt instanceof TryStatement ts)
|
||||||
|
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
for (Object stmt : block.statements()) {
|
||||||
|
if (stmt instanceof ExpressionStatement es)
|
||||||
|
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
else if (stmt instanceof VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragmentObj : vds.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
Expression initializer = fragment.getInitializer();
|
||||||
|
if (initializer != null) {
|
||||||
|
Object resolved = resolve(initializer, argsMap);
|
||||||
|
if (resolved != null)
|
||||||
|
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
||||||
|
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (stmt instanceof EnhancedForStatement efs)
|
||||||
|
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||||
|
else if (stmt instanceof TryStatement ts)
|
||||||
|
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
if (isWithStatesChain(mi, argsMap))
|
||||||
|
parseStateChain(mi, initialStates, endStates, argsMap);
|
||||||
|
else if (isForEach(mi))
|
||||||
|
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||||
|
else {
|
||||||
|
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof LambdaExpression lambda) {
|
||||||
|
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||||
|
if (calledMethod != null) {
|
||||||
|
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||||
|
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
|
||||||
|
Map<String, Object> map = new HashMap<>(currentArgsMap);
|
||||||
|
List<?> params = method.parameters();
|
||||||
|
List<?> args = call.arguments();
|
||||||
|
|
||||||
|
for (int i = 0; i < params.size(); i++) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
|
||||||
|
String paramName = param.getName().getIdentifier();
|
||||||
|
if (param.isVarargs() && i < params.size()) {
|
||||||
|
List<Expression> varargList = new ArrayList<>();
|
||||||
|
for (int j = i; j < args.size(); j++) {
|
||||||
|
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
|
||||||
|
if (resolved instanceof List<?> list) {
|
||||||
|
for (Object item : list) {
|
||||||
|
if (item instanceof Expression e)
|
||||||
|
varargList.add(e);
|
||||||
|
}
|
||||||
|
} else if (resolved instanceof Expression e) {
|
||||||
|
varargList.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map.put(paramName, varargList);
|
||||||
|
} else if (i < args.size()) {
|
||||||
|
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object resolve(Expression expr, Map<String, Object> argsMap) {
|
||||||
|
if (expr instanceof NullLiteral)
|
||||||
|
return null;
|
||||||
|
Set<Expression> visited = new HashSet<>();
|
||||||
|
Object current = expr;
|
||||||
|
while (current instanceof SimpleName sn) {
|
||||||
|
if (!visited.add((SimpleName) current))
|
||||||
|
break;
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved == null || resolved == current)
|
||||||
|
break;
|
||||||
|
current = resolved;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
|
||||||
|
Object resolved = resolve(expr, argsMap);
|
||||||
|
if (resolved instanceof Expression e)
|
||||||
|
return e;
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
|
||||||
|
Map<String, Expression> map = new HashMap<>();
|
||||||
|
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
|
||||||
|
if (entry.getValue() instanceof Expression e)
|
||||||
|
map.put(entry.getKey(), e);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTransitionChainEntry(MethodInvocation mi) {
|
||||||
|
Expression current = mi;
|
||||||
|
while (current instanceof MethodInvocation call) {
|
||||||
|
String name = call.getName().getIdentifier();
|
||||||
|
if (TransitionType.fromMethodName(name).isPresent())
|
||||||
|
return true;
|
||||||
|
current = call.getExpression();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
|
||||||
|
Expression current = mi;
|
||||||
|
Set<Expression> visited = new HashSet<>();
|
||||||
|
while (current instanceof MethodInvocation call) {
|
||||||
|
if (!visited.add(current))
|
||||||
|
break;
|
||||||
|
String name = call.getName().getIdentifier();
|
||||||
|
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
|
||||||
|
Expression receiver = call.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object resolved = resolve(receiver, argsMap);
|
||||||
|
if (resolved instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return resolved == null; // null means it's a parameter we track
|
||||||
|
}
|
||||||
|
if (TransitionType.fromMethodName(name).isPresent())
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
|
||||||
|
return findMethodInHierarchy(methodName, td, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
|
||||||
|
if (td == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (visited.contains(fqn))
|
||||||
|
return null;
|
||||||
|
visited.add(fqn);
|
||||||
|
|
||||||
|
for (MethodDeclaration method : td.getMethods())
|
||||||
|
if (method.getName().getIdentifier().equals(methodName))
|
||||||
|
return method;
|
||||||
|
|
||||||
|
Type superclassType = td.getSuperclassType();
|
||||||
|
if (superclassType != null) {
|
||||||
|
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
|
||||||
|
return findMethodInHierarchy(methodName, superclassTd, visited);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRelevantMethod(MethodDeclaration method) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
|
||||||
|
for (MethodDeclaration method : td.getMethods())
|
||||||
|
if (isConfigureTransitionsMethod(method))
|
||||||
|
return method;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
|
||||||
|
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
||||||
|
return false;
|
||||||
|
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
if (currentTd == null)
|
||||||
|
return;
|
||||||
|
String fqn = context.getFqn(currentTd);
|
||||||
|
if (visitedClasses.contains(fqn))
|
||||||
|
return;
|
||||||
|
visitedClasses.add(fqn);
|
||||||
|
|
||||||
|
for (MethodDeclaration method : currentTd.getMethods()) {
|
||||||
|
if (isConfigureStatesMethod(method)) {
|
||||||
|
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
||||||
|
|
||||||
|
// Hierarchy - Superclass
|
||||||
|
Type superclassType = currentTd.getSuperclassType();
|
||||||
|
if (superclassType != null) {
|
||||||
|
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
||||||
|
if (superclassTd != null)
|
||||||
|
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hierarchy - Interfaces
|
||||||
|
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
||||||
|
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||||
|
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
||||||
|
if (interfaceTd != null)
|
||||||
|
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
||||||
|
if (visitedMethods.contains(visit))
|
||||||
|
return;
|
||||||
|
visitedMethods.add(visit);
|
||||||
|
Block body = method.getBody();
|
||||||
|
if (body == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
|
||||||
|
Expression current = mi;
|
||||||
|
Set<Expression> visited = new HashSet<>();
|
||||||
|
while (current instanceof MethodInvocation call) {
|
||||||
|
if (!visited.add(current))
|
||||||
|
break;
|
||||||
|
if (call.getName().getIdentifier().equals("withStates"))
|
||||||
|
return true;
|
||||||
|
Expression receiver = call.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object resolved = resolve(receiver, argsMap);
|
||||||
|
if (resolved instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
|
||||||
|
List<MethodInvocation> chain = new ArrayList<>();
|
||||||
|
Expression current = mi;
|
||||||
|
while (current instanceof MethodInvocation m) {
|
||||||
|
chain.addFirst(m);
|
||||||
|
current = m.getExpression();
|
||||||
|
}
|
||||||
|
CompilationUnit cu = (CompilationUnit) mi.getRoot();
|
||||||
|
for (MethodInvocation call : chain) {
|
||||||
|
String methodName = call.getName().getIdentifier();
|
||||||
|
if (call.arguments().isEmpty())
|
||||||
|
continue;
|
||||||
|
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
|
||||||
|
if ("initial".equals(methodName)) {
|
||||||
|
if (!isInsideRegionOrFork(call)) {
|
||||||
|
initialStates.add(context.resolveState(arg, cu).toString());
|
||||||
|
}
|
||||||
|
} else if ("end".equals(methodName)) {
|
||||||
|
endStates.add(context.resolveState(arg, cu).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
||||||
|
Expression expr = mi.getExpression();
|
||||||
|
while (expr instanceof MethodInvocation parent) {
|
||||||
|
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
|
||||||
|
return true;
|
||||||
|
expr = parent.getExpression();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
|
||||||
|
for (MethodDeclaration method : td.getMethods())
|
||||||
|
if (isConfigureStatesMethod(method))
|
||||||
|
return method;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConfigureStatesMethod(MethodDeclaration method) {
|
||||||
|
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
||||||
|
return false;
|
||||||
|
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSimpleName(Type type) {
|
||||||
|
if (type.isSimpleType())
|
||||||
|
return ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
if (type.isParameterizedType())
|
||||||
|
return getSimpleName(((ParameterizedType) type).getType());
|
||||||
|
return type.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,632 +1,37 @@
|
|||||||
package click.kamil.springstatemachineexporter.ast.app;
|
package click.kamil.springstatemachineexporter.ast.app;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.eclipse.jdt.core.dom.ArrayInitializer;
|
|
||||||
import org.eclipse.jdt.core.dom.Block;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.EnhancedForStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.ExpressionStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.LambdaExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.NullLiteral;
|
|
||||||
import org.eclipse.jdt.core.dom.ParameterizedType;
|
|
||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
|
||||||
import org.eclipse.jdt.core.dom.SimpleType;
|
|
||||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TryStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.Type;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class StateMachineAggregator {
|
public class StateMachineAggregator {
|
||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
|
private final StateMachineAdapterParser adapterParser;
|
||||||
private enum AnalysisGoal {TRANSITIONS, STATES}
|
|
||||||
|
|
||||||
public StateMachineAggregator(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
|
|
||||||
List<Transition> allTransitions = new ArrayList<>();
|
|
||||||
Set<MethodVisit> visitedMethods = new HashSet<>();
|
|
||||||
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
|
|
||||||
return allTransitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
if (currentTd == null)
|
|
||||||
return;
|
|
||||||
String fqn = context.getFqn(currentTd);
|
|
||||||
if (visitedClasses.contains(fqn))
|
|
||||||
return;
|
|
||||||
visitedClasses.add(fqn);
|
|
||||||
|
|
||||||
for (MethodDeclaration method : currentTd.getMethods()) {
|
|
||||||
if (isConfigureTransitionsMethod(method)) {
|
|
||||||
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
|
||||||
|
|
||||||
// Hierarchy - Superclass
|
|
||||||
Type superclassType = currentTd.getSuperclassType();
|
|
||||||
if (superclassType != null) {
|
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
|
||||||
if (superclassTd != null) {
|
|
||||||
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hierarchy - Interfaces
|
|
||||||
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
|
||||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
|
||||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
|
||||||
if (interfaceTd != null) {
|
|
||||||
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
|
||||||
if (visitedMethods.contains(visit)) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
visitedMethods.add(visit);
|
|
||||||
|
|
||||||
Block body = method.getBody();
|
|
||||||
if (body == null) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
if (isTransitionChainEntry(mi)) {
|
|
||||||
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
|
|
||||||
} else if (isTransitionChainContinuation(mi, argsMap)) {
|
|
||||||
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
|
|
||||||
} else if (isForEach(mi)) {
|
|
||||||
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
|
||||||
} else {
|
|
||||||
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
|
||||||
Expression lambdaReceiver = mi.getExpression();
|
|
||||||
if (lambdaReceiver instanceof SimpleName sn) {
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved instanceof LambdaExpression lambda) {
|
|
||||||
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
|
||||||
if (calledMethod == null) {
|
|
||||||
// Try to resolve method in another class if it's a static call or on a known type
|
|
||||||
Expression miReceiver = mi.getExpression();
|
|
||||||
if (miReceiver instanceof SimpleName sn) {
|
|
||||||
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
|
|
||||||
if (otherTd != null) {
|
|
||||||
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
|
|
||||||
if (calledMethod != null) {
|
|
||||||
// For calls to other classes, we switch the "searchStartClass" to that class
|
|
||||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
|
||||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
|
|
||||||
return transitions; // Handled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calledMethod != null) {
|
|
||||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
|
||||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isForEach(MethodInvocation mi) {
|
|
||||||
return mi.getName().getIdentifier().equals("forEach");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
|
||||||
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
|
||||||
List<?> params = lambda.parameters();
|
|
||||||
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
|
|
||||||
Object p = params.get(i);
|
|
||||||
String paramName = "";
|
|
||||||
if (p instanceof VariableDeclaration vp)
|
|
||||||
paramName = vp.getName().getIdentifier();
|
|
||||||
if (!paramName.isEmpty()) {
|
|
||||||
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
|
||||||
if (lambda.getBody() instanceof Block block)
|
|
||||||
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
|
|
||||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
|
||||||
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
|
|
||||||
} else {
|
|
||||||
if (lambda.getBody() instanceof Block block)
|
|
||||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
|
||||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
|
||||||
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
Expression receiver = forEachMi.getExpression();
|
|
||||||
List<Expression> elements = unrollCollection(receiver, argsMap);
|
|
||||||
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
|
|
||||||
return transitions;
|
|
||||||
|
|
||||||
Object lambdaObj = forEachMi.arguments().get(0);
|
|
||||||
if (lambdaObj instanceof LambdaExpression lambda) {
|
|
||||||
String paramName = getLambdaParamName(lambda);
|
|
||||||
for (Expression element : elements) {
|
|
||||||
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
|
||||||
if (!paramName.isEmpty())
|
|
||||||
lambdaArgsMap.put(paramName, element);
|
|
||||||
|
|
||||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
|
||||||
if (lambda.getBody() instanceof Block block)
|
|
||||||
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
|
|
||||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
|
||||||
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
|
|
||||||
} else {
|
|
||||||
if (lambda.getBody() instanceof Block block)
|
|
||||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
|
||||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
|
||||||
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
|
|
||||||
String paramName = efs.getParameter().getName().getIdentifier();
|
|
||||||
|
|
||||||
for (Expression element : elements) {
|
|
||||||
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
|
|
||||||
loopArgsMap.put(paramName, element);
|
|
||||||
|
|
||||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
|
||||||
if (efs.getBody() instanceof Block block)
|
|
||||||
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
|
|
||||||
else if (efs.getBody() instanceof ExpressionStatement es)
|
|
||||||
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
|
|
||||||
} else {
|
|
||||||
if (efs.getBody() instanceof Block block)
|
|
||||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
|
||||||
else if (efs.getBody() instanceof ExpressionStatement es)
|
|
||||||
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
|
|
||||||
Set<Expression> visited = new HashSet<>();
|
|
||||||
Expression current = expr;
|
|
||||||
while (current != null) {
|
|
||||||
if (!visited.add(current))
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (current instanceof MethodInvocation mi) {
|
|
||||||
String name = mi.getName().getIdentifier();
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|
|
||||||
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
|
|
||||||
if (mi.arguments().size() == 1) {
|
|
||||||
Expression arg = (Expression) mi.arguments().get(0);
|
|
||||||
if (arg instanceof SimpleName sn) {
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved instanceof List list)
|
|
||||||
return (List<Expression>) list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (List<Expression>) mi.arguments();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current instanceof SimpleName sn) {
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved instanceof List list)
|
|
||||||
return (List<Expression>) list;
|
|
||||||
if (resolved instanceof Expression e) {
|
|
||||||
current = e;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current instanceof ArrayInitializer ai)
|
|
||||||
return (List<Expression>) ai.expressions();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getLambdaParamName(LambdaExpression lambda) {
|
|
||||||
if (lambda.parameters().size() == 1) {
|
|
||||||
Object p = lambda.parameters().get(0);
|
|
||||||
if (p instanceof VariableDeclaration vp)
|
|
||||||
return vp.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
for (Object stmt : block.statements()) {
|
|
||||||
if (stmt instanceof ExpressionStatement es)
|
|
||||||
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
|
|
||||||
else if (stmt instanceof VariableDeclarationStatement vds) {
|
|
||||||
for (Object fragmentObj : vds.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
|
||||||
Expression initializer = fragment.getInitializer();
|
|
||||||
if (initializer != null) {
|
|
||||||
Object resolved = resolve(initializer, argsMap);
|
|
||||||
if (resolved != null)
|
|
||||||
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
|
||||||
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (stmt instanceof EnhancedForStatement efs)
|
|
||||||
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
|
||||||
else if (stmt instanceof TryStatement ts)
|
|
||||||
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
for (Object stmt : block.statements()) {
|
|
||||||
if (stmt instanceof ExpressionStatement es)
|
|
||||||
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
else if (stmt instanceof VariableDeclarationStatement vds) {
|
|
||||||
for (Object fragmentObj : vds.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
|
||||||
Expression initializer = fragment.getInitializer();
|
|
||||||
if (initializer != null) {
|
|
||||||
Object resolved = resolve(initializer, argsMap);
|
|
||||||
if (resolved != null)
|
|
||||||
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
|
||||||
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (stmt instanceof EnhancedForStatement efs)
|
|
||||||
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
|
||||||
else if (stmt instanceof TryStatement ts)
|
|
||||||
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
if (isWithStatesChain(mi, argsMap))
|
|
||||||
parseStateChain(mi, initialStates, endStates, argsMap);
|
|
||||||
else if (isForEach(mi))
|
|
||||||
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
|
||||||
else {
|
|
||||||
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved instanceof LambdaExpression lambda) {
|
|
||||||
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
|
||||||
if (calledMethod != null) {
|
|
||||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
|
||||||
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
|
|
||||||
Map<String, Object> map = new HashMap<>(currentArgsMap);
|
|
||||||
List<?> params = method.parameters();
|
|
||||||
List<?> args = call.arguments();
|
|
||||||
|
|
||||||
for (int i = 0; i < params.size(); i++) {
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
|
|
||||||
String paramName = param.getName().getIdentifier();
|
|
||||||
if (param.isVarargs() && i < params.size()) {
|
|
||||||
List<Expression> varargList = new ArrayList<>();
|
|
||||||
for (int j = i; j < args.size(); j++) {
|
|
||||||
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
|
|
||||||
if (resolved instanceof List<?> list) {
|
|
||||||
for (Object item : list) {
|
|
||||||
if (item instanceof Expression e)
|
|
||||||
varargList.add(e);
|
|
||||||
}
|
|
||||||
} else if (resolved instanceof Expression e) {
|
|
||||||
varargList.add(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
map.put(paramName, varargList);
|
|
||||||
} else if (i < args.size()) {
|
|
||||||
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object resolve(Expression expr, Map<String, Object> argsMap) {
|
|
||||||
if (expr instanceof NullLiteral)
|
|
||||||
return null;
|
|
||||||
Set<Expression> visited = new HashSet<>();
|
|
||||||
Object current = expr;
|
|
||||||
while (current instanceof SimpleName sn) {
|
|
||||||
if (!visited.add((SimpleName) current))
|
|
||||||
break;
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved == null || resolved == current)
|
|
||||||
break;
|
|
||||||
current = resolved;
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
|
|
||||||
Object resolved = resolve(expr, argsMap);
|
|
||||||
if (resolved instanceof Expression e)
|
|
||||||
return e;
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
|
|
||||||
Map<String, Expression> map = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
|
|
||||||
if (entry.getValue() instanceof Expression e)
|
|
||||||
map.put(entry.getKey(), e);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTransitionChainEntry(MethodInvocation mi) {
|
|
||||||
Expression current = mi;
|
|
||||||
while (current instanceof MethodInvocation call) {
|
|
||||||
String name = call.getName().getIdentifier();
|
|
||||||
if (TransitionType.fromMethodName(name).isPresent())
|
|
||||||
return true;
|
|
||||||
current = call.getExpression();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
|
|
||||||
Expression current = mi;
|
|
||||||
Set<Expression> visited = new HashSet<>();
|
|
||||||
while (current instanceof MethodInvocation call) {
|
|
||||||
if (!visited.add(current))
|
|
||||||
break;
|
|
||||||
String name = call.getName().getIdentifier();
|
|
||||||
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
|
|
||||||
Expression receiver = call.getExpression();
|
|
||||||
if (receiver instanceof MethodInvocation next) {
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Object resolved = resolve(receiver, argsMap);
|
|
||||||
if (resolved instanceof MethodInvocation next) {
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return resolved == null; // null means it's a parameter we track
|
|
||||||
}
|
|
||||||
if (TransitionType.fromMethodName(name).isPresent())
|
|
||||||
return true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
|
|
||||||
return findMethodInHierarchy(methodName, td, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
|
|
||||||
if (td == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.contains(fqn))
|
|
||||||
return null;
|
|
||||||
visited.add(fqn);
|
|
||||||
|
|
||||||
for (MethodDeclaration method : td.getMethods())
|
|
||||||
if (method.getName().getIdentifier().equals(methodName))
|
|
||||||
return method;
|
|
||||||
|
|
||||||
Type superclassType = td.getSuperclassType();
|
|
||||||
if (superclassType != null) {
|
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
|
|
||||||
return findMethodInHierarchy(methodName, superclassTd, visited);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isRelevantMethod(MethodDeclaration method) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
|
|
||||||
for (MethodDeclaration method : td.getMethods())
|
|
||||||
if (isConfigureTransitionsMethod(method))
|
|
||||||
return method;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
|
|
||||||
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
|
||||||
return false;
|
|
||||||
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final Set<String> initialStates = new HashSet<>();
|
private final Set<String> initialStates = new HashSet<>();
|
||||||
@Getter
|
@Getter
|
||||||
private final Set<String> endStates = new HashSet<>();
|
private final Set<String> endStates = new HashSet<>();
|
||||||
|
|
||||||
|
public StateMachineAggregator(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
this.adapterParser = new StateMachineAdapterParser(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
|
||||||
|
return adapterParser.parseTransitions(startClass);
|
||||||
|
}
|
||||||
|
|
||||||
public void aggregateStates(TypeDeclaration startClass) {
|
public void aggregateStates(TypeDeclaration startClass) {
|
||||||
Set<MethodVisit> visitedMethods = new HashSet<>();
|
StateMachineAdapterParser.AdapterStates states = adapterParser.parseStates(startClass);
|
||||||
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
|
this.initialStates.clear();
|
||||||
}
|
this.initialStates.addAll(states.initialStates());
|
||||||
|
this.endStates.clear();
|
||||||
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
this.endStates.addAll(states.endStates());
|
||||||
if (currentTd == null)
|
|
||||||
return;
|
|
||||||
String fqn = context.getFqn(currentTd);
|
|
||||||
if (visitedClasses.contains(fqn))
|
|
||||||
return;
|
|
||||||
visitedClasses.add(fqn);
|
|
||||||
|
|
||||||
for (MethodDeclaration method : currentTd.getMethods()) {
|
|
||||||
if (isConfigureStatesMethod(method)) {
|
|
||||||
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
|
||||||
|
|
||||||
// Hierarchy - Superclass
|
|
||||||
Type superclassType = currentTd.getSuperclassType();
|
|
||||||
if (superclassType != null) {
|
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
|
||||||
if (superclassTd != null)
|
|
||||||
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hierarchy - Interfaces
|
|
||||||
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
|
||||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
|
||||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
|
||||||
if (interfaceTd != null)
|
|
||||||
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
|
||||||
if (visitedMethods.contains(visit))
|
|
||||||
return;
|
|
||||||
visitedMethods.add(visit);
|
|
||||||
Block body = method.getBody();
|
|
||||||
if (body == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
|
|
||||||
Expression current = mi;
|
|
||||||
Set<Expression> visited = new HashSet<>();
|
|
||||||
while (current instanceof MethodInvocation call) {
|
|
||||||
if (!visited.add(current))
|
|
||||||
break;
|
|
||||||
if (call.getName().getIdentifier().equals("withStates"))
|
|
||||||
return true;
|
|
||||||
Expression receiver = call.getExpression();
|
|
||||||
if (receiver instanceof MethodInvocation next) {
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Object resolved = resolve(receiver, argsMap);
|
|
||||||
if (resolved instanceof MethodInvocation next) {
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
|
|
||||||
List<MethodInvocation> chain = new ArrayList<>();
|
|
||||||
Expression current = mi;
|
|
||||||
while (current instanceof MethodInvocation m) {
|
|
||||||
chain.addFirst(m);
|
|
||||||
current = m.getExpression();
|
|
||||||
}
|
|
||||||
CompilationUnit cu = (CompilationUnit) mi.getRoot();
|
|
||||||
for (MethodInvocation call : chain) {
|
|
||||||
String methodName = call.getName().getIdentifier();
|
|
||||||
if (call.arguments().isEmpty())
|
|
||||||
continue;
|
|
||||||
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
|
|
||||||
if ("initial".equals(methodName)) {
|
|
||||||
if (!isInsideRegionOrFork(call)) {
|
|
||||||
initialStates.add(context.resolveState(arg, cu).toString());
|
|
||||||
}
|
|
||||||
} else if ("end".equals(methodName)) {
|
|
||||||
endStates.add(context.resolveState(arg, cu).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
|
||||||
Expression expr = mi.getExpression();
|
|
||||||
while (expr instanceof MethodInvocation parent) {
|
|
||||||
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
|
|
||||||
return true;
|
|
||||||
expr = parent.getExpression();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
|
|
||||||
for (MethodDeclaration method : td.getMethods())
|
|
||||||
if (isConfigureStatesMethod(method))
|
|
||||||
return method;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConfigureStatesMethod(MethodDeclaration method) {
|
|
||||||
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
|
||||||
return false;
|
|
||||||
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getSimpleName(Type type) {
|
|
||||||
if (type.isSimpleType())
|
|
||||||
return ((SimpleType) type).getName().getFullyQualifiedName();
|
|
||||||
if (type.isParameterizedType())
|
|
||||||
return getSimpleName(((ParameterizedType) type).getType());
|
|
||||||
return type.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,4 +36,63 @@ public final class AstUtils {
|
|||||||
return type.toString(); // fallback
|
return type.toString(); // fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isFunctionalCollectionMethod(org.eclipse.jdt.core.dom.MethodInvocation node) {
|
||||||
|
if (node.getExpression() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding typeBinding = node.getExpression().resolveTypeBinding();
|
||||||
|
if (typeBinding != null) {
|
||||||
|
if (isCollectionOrStreamType(typeBinding)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
if (methodName.equals("forEach") || methodName.equals("map") ||
|
||||||
|
methodName.equals("filter") || methodName.equals("flatMap") ||
|
||||||
|
methodName.equals("anyMatch") || methodName.equals("allMatch") ||
|
||||||
|
methodName.equals("noneMatch") || methodName.equals("reduce") ||
|
||||||
|
methodName.equals("collect") || methodName.equals("removeIf") ||
|
||||||
|
methodName.equals("ifPresent") || methodName.equals("ifPresentOrElse") ||
|
||||||
|
methodName.equals("compute") || methodName.equals("computeIfAbsent") ||
|
||||||
|
methodName.equals("computeIfPresent") || methodName.equals("replaceAll") ||
|
||||||
|
methodName.equals("merge") || methodName.equals("peek") ||
|
||||||
|
methodName.equals("doOnNext") || methodName.equals("doOnSuccess") ||
|
||||||
|
methodName.equals("doOnError") || methodName.equals("concatMap") ||
|
||||||
|
methodName.equals("switchMap")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCollectionOrStreamType(org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
|
||||||
|
if (typeBinding == null) return false;
|
||||||
|
String fqn = typeBinding.getErasure().getQualifiedName();
|
||||||
|
if (fqn.startsWith("java.util.stream.") ||
|
||||||
|
fqn.equals("java.lang.Iterable") ||
|
||||||
|
fqn.equals("java.util.Collection") ||
|
||||||
|
fqn.equals("java.util.List") ||
|
||||||
|
fqn.equals("java.util.Set") ||
|
||||||
|
fqn.equals("java.util.Map") ||
|
||||||
|
fqn.equals("java.util.Iterator") ||
|
||||||
|
fqn.equals("java.util.Optional") ||
|
||||||
|
fqn.equals("reactor.core.publisher.Flux") ||
|
||||||
|
fqn.equals("reactor.core.publisher.Mono")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (org.eclipse.jdt.core.dom.ITypeBinding intf : typeBinding.getInterfaces()) {
|
||||||
|
if (isCollectionOrStreamType(intf)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeBinding.getSuperclass() != null) {
|
||||||
|
return isCollectionOrStreamType(typeBinding.getSuperclass());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class BindingResolver {
|
||||||
|
|
||||||
|
private BindingResolver() {
|
||||||
|
// Prevent instantiation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding for a given AST Type node.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(Type type) {
|
||||||
|
if (type == null) return null;
|
||||||
|
return type.resolveBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding for a given AST Expression node.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(Expression expression) {
|
||||||
|
if (expression == null) return null;
|
||||||
|
return expression.resolveTypeBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the method binding for a MethodInvocation.
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveMethod(MethodInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveMethodBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a ClassInstanceCreation.
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(ClassInstanceCreation creation) {
|
||||||
|
if (creation == null) return null;
|
||||||
|
return creation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a ConstructorInvocation (e.g. this(...)).
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(ConstructorInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a SuperConstructorInvocation (e.g. super(...)).
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(SuperConstructorInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding of a TypeDeclaration.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(TypeDeclaration td) {
|
||||||
|
if (td == null) return null;
|
||||||
|
return td.resolveBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the fully qualified name of a type binding, safely handling nulls and erasure.
|
||||||
|
*/
|
||||||
|
public static String getFullyQualifiedName(ITypeBinding binding) {
|
||||||
|
if (binding == null) return null;
|
||||||
|
return binding.getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn) {
|
||||||
|
return isSubtypeOf(childBinding, targetFqn, new java.util.HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn, Set<String> visited) {
|
||||||
|
if (childBinding == null || targetFqn == null) return false;
|
||||||
|
|
||||||
|
String key = childBinding.getKey();
|
||||||
|
if (key != null && !visited.add(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct match
|
||||||
|
if (targetFqn.equals(childBinding.getErasure().getQualifiedName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check interfaces
|
||||||
|
for (ITypeBinding intf : childBinding.getInterfaces()) {
|
||||||
|
if (isSubtypeOf(intf, targetFqn, visited)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check superclass
|
||||||
|
ITypeBinding superclass = childBinding.getSuperclass();
|
||||||
|
if (superclass != null) {
|
||||||
|
return isSubtypeOf(superclass, targetFqn, visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class CfgBuilder {
|
||||||
|
|
||||||
|
public static ControlFlowGraph build(MethodDeclaration method) {
|
||||||
|
ControlFlowGraph cfg = new ControlFlowGraph(method);
|
||||||
|
if (method.getBody() == null) {
|
||||||
|
cfg.getEntryNode().addSuccessor(cfg.getExitNode());
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CFG recursively
|
||||||
|
List<ControlFlowGraph.CfgNode> exits = buildFlow(method.getBody(), List.of(cfg.getEntryNode()), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : exits) {
|
||||||
|
exit.addSuccessor(cfg.getExitNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ControlFlowGraph.CfgNode> buildFlow(
|
||||||
|
ASTNode node,
|
||||||
|
List<ControlFlowGraph.CfgNode> sources,
|
||||||
|
ControlFlowGraph cfg) {
|
||||||
|
|
||||||
|
if (node == null || sources.isEmpty()) {
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof Block block) {
|
||||||
|
List<ControlFlowGraph.CfgNode> currentSources = sources;
|
||||||
|
for (Object stmtObj : block.statements()) {
|
||||||
|
currentSources = buildFlow((ASTNode) stmtObj, currentSources, cfg);
|
||||||
|
}
|
||||||
|
return currentSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof IfStatement ifStmt) {
|
||||||
|
// Condition node
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(ifStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> thenExits = buildFlow(ifStmt.getThenStatement(), List.of(condNode), cfg);
|
||||||
|
List<ControlFlowGraph.CfgNode> elseExits;
|
||||||
|
if (ifStmt.getElseStatement() != null) {
|
||||||
|
elseExits = buildFlow(ifStmt.getElseStatement(), List.of(condNode), cfg);
|
||||||
|
} else {
|
||||||
|
elseExits = List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> combinedExits = new ArrayList<>();
|
||||||
|
combinedExits.addAll(thenExits);
|
||||||
|
combinedExits.addAll(elseExits);
|
||||||
|
return combinedExits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof WhileStatement whileStmt) {
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(whileStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(whileStmt.getBody(), List.of(condNode), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(condNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(condNode); // Exit loop when cond is false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof DoStatement doStmt) {
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodySources = new ArrayList<>(sources);
|
||||||
|
bodySources.add(condNode);
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(doStmt.getBody(), bodySources, cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
return List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof ForStatement forStmt) {
|
||||||
|
// Initializers
|
||||||
|
List<ControlFlowGraph.CfgNode> currentSources = sources;
|
||||||
|
for (Object initObj : forStmt.initializers()) {
|
||||||
|
ControlFlowGraph.CfgNode initNode = new ControlFlowGraph.CfgNode((ASTNode) initObj, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(initNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : currentSources) {
|
||||||
|
src.addSuccessor(initNode);
|
||||||
|
}
|
||||||
|
currentSources = List.of(initNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition
|
||||||
|
ControlFlowGraph.CfgNode condNode;
|
||||||
|
if (forStmt.getExpression() != null) {
|
||||||
|
condNode = new ControlFlowGraph.CfgNode(forStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
} else {
|
||||||
|
condNode = new ControlFlowGraph.CfgNode(forStmt, ControlFlowGraph.CfgNodeType.STATEMENT); // Dummy node
|
||||||
|
}
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : currentSources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(forStmt.getBody(), List.of(condNode), cfg);
|
||||||
|
|
||||||
|
// Updaters
|
||||||
|
List<ControlFlowGraph.CfgNode> updaterSources = bodyExits;
|
||||||
|
for (Object updateObj : forStmt.updaters()) {
|
||||||
|
ControlFlowGraph.CfgNode updateNode = new ControlFlowGraph.CfgNode((ASTNode) updateObj, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(updateNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : updaterSources) {
|
||||||
|
src.addSuccessor(updateNode);
|
||||||
|
}
|
||||||
|
updaterSources = List.of(updateNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode src : updaterSources) {
|
||||||
|
src.addSuccessor(condNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof EnhancedForStatement effStmt) {
|
||||||
|
ControlFlowGraph.CfgNode loopNode = new ControlFlowGraph.CfgNode(effStmt.getParameter(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(loopNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(loopNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(effStmt.getBody(), List.of(loopNode), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(loopNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(loopNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof ReturnStatement returnStmt) {
|
||||||
|
ControlFlowGraph.CfgNode retNode = new ControlFlowGraph.CfgNode(returnStmt, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(retNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(retNode);
|
||||||
|
}
|
||||||
|
// Return flows to exit, not to subsequent sequential nodes
|
||||||
|
retNode.addSuccessor(cfg.getExitNode());
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: general statement
|
||||||
|
ControlFlowGraph.CfgNode stmtNode = new ControlFlowGraph.CfgNode(node, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(stmtNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(stmtNode);
|
||||||
|
}
|
||||||
|
return List.of(stmtNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,12 @@ public class CodebaseContext {
|
|||||||
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
||||||
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
||||||
private final ConstantResolver constantResolver = new ConstantResolver();
|
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||||
|
private final StateResolver stateResolver = new StateResolver(this);
|
||||||
private final PropertyResolver propertyResolver = new PropertyResolver();
|
private final PropertyResolver propertyResolver = new PropertyResolver();
|
||||||
|
|
||||||
|
public ConstantResolver getConstantResolver() {
|
||||||
|
return constantResolver;
|
||||||
|
}
|
||||||
private final List<String> activeProfiles = new ArrayList<>();
|
private final List<String> activeProfiles = new ArrayList<>();
|
||||||
private Map<String, Map<String, String>> allProperties = new HashMap<>();
|
private Map<String, Map<String, String>> allProperties = new HashMap<>();
|
||||||
private List<LibraryHint> libraryHints = new ArrayList<>();
|
private List<LibraryHint> libraryHints = new ArrayList<>();
|
||||||
@@ -65,7 +70,7 @@ public class CodebaseContext {
|
|||||||
return Collections.unmodifiableList(libraryHints);
|
return Collections.unmodifiableList(libraryHints);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/**", "**/node_modules/**", "**/out/**"));
|
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/classes/**", "**/build/libs/**", "**/build/tmp/**", "**/build/reports/**", "**/build/test-results/**", "**/node_modules/**", "**/out/**"));
|
||||||
private String[] classpath = new String[0];
|
private String[] classpath = new String[0];
|
||||||
private String[] sourcepath = new String[0];
|
private String[] sourcepath = new String[0];
|
||||||
private boolean resolveBindings = false;
|
private boolean resolveBindings = false;
|
||||||
@@ -135,7 +140,7 @@ public class CodebaseContext {
|
|||||||
if (imp.isOnDemand()) {
|
if (imp.isOnDemand()) {
|
||||||
// Star import: import static com.example.C.*;
|
// Star import: import static com.example.C.*;
|
||||||
TypeDeclaration td = getTypeDeclaration(impName, contextCu);
|
TypeDeclaration td = getTypeDeclaration(impName, contextCu);
|
||||||
if (td != null && hasField(td, memberName)) {
|
if (td != null && (hasField(td, memberName) || hasMethod(td, memberName))) {
|
||||||
return td;
|
return td;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -150,6 +155,15 @@ public class CodebaseContext {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasMethod(TypeDeclaration td, String name) {
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.getName().getIdentifier().equals(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasField(TypeDeclaration td, String name) {
|
public boolean hasField(TypeDeclaration td, String name) {
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
for (Object fragObj : fd.fragments()) {
|
for (Object fragObj : fd.fragments()) {
|
||||||
@@ -191,6 +205,8 @@ public class CodebaseContext {
|
|||||||
indexType(td, packageName, javaFile);
|
indexType(td, packageName, javaFile);
|
||||||
} else if (type instanceof EnumDeclaration ed) {
|
} else if (type instanceof EnumDeclaration ed) {
|
||||||
indexEnum(ed, packageName, javaFile);
|
indexEnum(ed, packageName, javaFile);
|
||||||
|
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration rd) {
|
||||||
|
indexRecord(rd, packageName, javaFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,9 +244,42 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recursively index nested types
|
// Recursively index nested types
|
||||||
for (Object type : td.getTypes()) {
|
for (Object decl : td.bodyDeclarations()) {
|
||||||
if (type instanceof TypeDeclaration nestedTd) {
|
if (decl instanceof TypeDeclaration nestedTd) {
|
||||||
indexType(nestedTd, fqn, javaFile);
|
indexType(nestedTd, fqn, javaFile);
|
||||||
|
} else if (decl instanceof EnumDeclaration nestedEd) {
|
||||||
|
indexEnum(nestedEd, fqn, javaFile);
|
||||||
|
} else if (decl instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
|
||||||
|
indexRecord(nestedRd, fqn, javaFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void indexRecord(org.eclipse.jdt.core.dom.RecordDeclaration rd, String parentFqn, Path javaFile) {
|
||||||
|
String simpleName = rd.getName().getIdentifier();
|
||||||
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|
||||||
|
// Treat as a class since it is a type
|
||||||
|
classes.put(fqn, (CompilationUnit) rd.getRoot());
|
||||||
|
classPaths.put(fqn, javaFile);
|
||||||
|
|
||||||
|
if (simpleNameToFqn.containsKey(simpleName)) {
|
||||||
|
String existingFqn = simpleNameToFqn.get(simpleName);
|
||||||
|
if (!existingFqn.equals(fqn)) {
|
||||||
|
ambiguousSimpleNames.add(simpleName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
simpleNameToFqn.put(simpleName, fqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively index nested types
|
||||||
|
for (Object decl : rd.bodyDeclarations()) {
|
||||||
|
if (decl instanceof TypeDeclaration nestedTd) {
|
||||||
|
indexType(nestedTd, fqn, javaFile);
|
||||||
|
} else if (decl instanceof EnumDeclaration nestedEd) {
|
||||||
|
indexEnum(nestedEd, fqn, javaFile);
|
||||||
|
} else if (decl instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
|
||||||
|
indexRecord(nestedRd, fqn, javaFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,123 +337,44 @@ public class CodebaseContext {
|
|||||||
if (!simpleNameToFqn.containsKey(simpleName)) {
|
if (!simpleNameToFqn.containsKey(simpleName)) {
|
||||||
simpleNameToFqn.put(simpleName, fqn);
|
simpleNameToFqn.put(simpleName, fqn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively index nested types inside enum body
|
||||||
|
for (Object decl : ed.bodyDeclarations()) {
|
||||||
|
if (decl instanceof TypeDeclaration nestedTd) {
|
||||||
|
indexType(nestedTd, fqn, javaFile);
|
||||||
|
} else if (decl instanceof EnumDeclaration nestedEd) {
|
||||||
|
indexEnum(nestedEd, fqn, javaFile);
|
||||||
|
} else if (decl instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
|
||||||
|
indexRecord(nestedRd, fqn, javaFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<String>> getEnumValuesMap() {
|
||||||
|
return enumValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getEnumValues(String fqnOrSimpleName) {
|
public List<String> getEnumValues(String fqnOrSimpleName) {
|
||||||
if (fqnOrSimpleName == null) return null;
|
if (fqnOrSimpleName == null) return null;
|
||||||
List<String> values = enumValues.get(fqnOrSimpleName);
|
|
||||||
|
String cleanName = fqnOrSimpleName;
|
||||||
|
if (cleanName.contains("<")) {
|
||||||
|
cleanName = cleanName.substring(0, cleanName.indexOf('<'));
|
||||||
|
}
|
||||||
|
if (cleanName.contains("[")) {
|
||||||
|
cleanName = cleanName.substring(0, cleanName.indexOf('['));
|
||||||
|
}
|
||||||
|
cleanName = cleanName.trim();
|
||||||
|
|
||||||
|
List<String> values = enumValues.get(cleanName);
|
||||||
if (values != null) return values;
|
if (values != null) return values;
|
||||||
String fqn = simpleNameToFqn.get(fqnOrSimpleName);
|
String fqn = simpleNameToFqn.get(cleanName);
|
||||||
if (fqn != null) return enumValues.get(fqn);
|
if (fqn != null) return enumValues.get(fqn);
|
||||||
// Also try stripping array or generic types if any (e.g. MyEnum[])
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public State resolveState(Expression expr, CompilationUnit cu) {
|
public State resolveState(Expression expr, CompilationUnit cu) {
|
||||||
if (expr == null)
|
return stateResolver.resolveState(expr, cu);
|
||||||
return null;
|
|
||||||
|
|
||||||
// 1. Check for constants
|
|
||||||
String resolvedValue = constantResolver.resolve(expr, this);
|
|
||||||
if (resolvedValue != null) {
|
|
||||||
return State.of(expr.toString(), resolvedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check for @Value fields (SimpleName)
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
String fieldName = sn.getIdentifier();
|
|
||||||
TypeDeclaration td = findEnclosingType(sn);
|
|
||||||
if (td != null) {
|
|
||||||
String value = findValueFromField(td, fieldName);
|
|
||||||
if (value != null) {
|
|
||||||
return State.of(fieldName, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expr instanceof StringLiteral sl)
|
|
||||||
return State.of(sl.getLiteralValue());
|
|
||||||
|
|
||||||
String raw = expr.toString();
|
|
||||||
if (expr instanceof QualifiedName qn) {
|
|
||||||
String full = resolveQualifiedName(qn, cu);
|
|
||||||
return State.of(raw, full);
|
|
||||||
}
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
String full = resolveSimpleName(sn, cu);
|
|
||||||
return State.of(raw, full);
|
|
||||||
}
|
|
||||||
return State.of(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findValueFromField(TypeDeclaration td, String fieldName) {
|
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
|
||||||
if (fragment.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
// Check for @Value annotation
|
|
||||||
for (Object mod : fd.modifiers()) {
|
|
||||||
if (mod instanceof Annotation ann) {
|
|
||||||
String name = ann.getTypeName().getFullyQualifiedName();
|
|
||||||
if (name.endsWith("Value")) {
|
|
||||||
return extractAnnotationValue(ann);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractAnnotationValue(Annotation ann) {
|
|
||||||
if (ann instanceof SingleMemberAnnotation sma) {
|
|
||||||
return stripQuotes(sma.getValue().toString());
|
|
||||||
} else if (ann instanceof NormalAnnotation na) {
|
|
||||||
for (Object pairObj : na.values()) {
|
|
||||||
MemberValuePair pair = (MemberValuePair) pairObj;
|
|
||||||
if ("value".equals(pair.getName().getIdentifier())) {
|
|
||||||
return stripQuotes(pair.getValue().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String stripQuotes(String s) {
|
|
||||||
if (s == null) return null;
|
|
||||||
return s.replaceAll("^\"|\"$", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
|
||||||
ASTNode parent = node.getParent();
|
|
||||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
|
||||||
parent = parent.getParent();
|
|
||||||
}
|
|
||||||
return (TypeDeclaration) parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
|
|
||||||
String qualifier = qn.getQualifier().toString();
|
|
||||||
String name = qn.getName().getIdentifier();
|
|
||||||
TypeDeclaration td = getTypeDeclaration(qualifier, cu);
|
|
||||||
if (td != null)
|
|
||||||
return getFqn(td) + "." + name;
|
|
||||||
return qn.getFullyQualifiedName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveSimpleName(SimpleName sn, CompilationUnit cu) {
|
|
||||||
String name = sn.getIdentifier();
|
|
||||||
if (cu == null)
|
|
||||||
return name;
|
|
||||||
for (Object impObj : cu.imports()) {
|
|
||||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
|
||||||
String impName = imp.getName().getFullyQualifiedName();
|
|
||||||
if (impName.endsWith("." + name))
|
|
||||||
return impName;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompilationUnit parse(String source, String unitName) {
|
private CompilationUnit parse(String source, String unitName) {
|
||||||
@@ -412,6 +382,7 @@ public class CodebaseContext {
|
|||||||
parser.setSource(source.toCharArray());
|
parser.setSource(source.toCharArray());
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
parser.setResolveBindings(resolveBindings);
|
parser.setResolveBindings(resolveBindings);
|
||||||
|
parser.setBindingsRecovery(true);
|
||||||
|
|
||||||
Map<String, String> options = JavaCore.getOptions();
|
Map<String, String> options = JavaCore.getOptions();
|
||||||
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
|
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
|
||||||
@@ -457,6 +428,14 @@ public class CodebaseContext {
|
|||||||
|
|
||||||
// 2. Check imports in contextCu
|
// 2. Check imports in contextCu
|
||||||
if (contextCu != null) {
|
if (contextCu != null) {
|
||||||
|
for (Object typeObj : contextCu.types()) {
|
||||||
|
if (typeObj instanceof TypeDeclaration tdDecl) {
|
||||||
|
if (tdDecl.getName().getIdentifier().equals(name)) {
|
||||||
|
return tdDecl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (Object impObj : contextCu.imports()) {
|
for (Object impObj : contextCu.imports()) {
|
||||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
String impName = imp.getName().getFullyQualifiedName();
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
@@ -493,11 +472,20 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getSuperclassFqn(TypeDeclaration td) {
|
public String getSuperclassFqn(TypeDeclaration td) {
|
||||||
|
if (td == null) return null;
|
||||||
|
ITypeBinding binding = td.resolveBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding superBinding = binding.getSuperclass();
|
||||||
|
if (superBinding != null) {
|
||||||
|
return superBinding.getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Type superType = td.getSuperclassType();
|
Type superType = td.getSuperclassType();
|
||||||
if (superType == null) return null;
|
if (superType == null) return null;
|
||||||
|
|
||||||
String superName = extractTypeName(superType);
|
String superName = extractTypeName(superType);
|
||||||
CompilationUnit cu = (CompilationUnit) td.getRoot();
|
CompilationUnit cu = (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null;
|
||||||
TypeDeclaration superTd = getTypeDeclaration(superName, cu);
|
TypeDeclaration superTd = getTypeDeclaration(superName, cu);
|
||||||
return superTd != null ? getFqn(superTd) : superName;
|
return superTd != null ? getFqn(superTd) : superName;
|
||||||
}
|
}
|
||||||
@@ -514,6 +502,56 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
|
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||||
|
if (td == null) return null;
|
||||||
|
ITypeBinding binding = td.resolveBinding();
|
||||||
|
if (binding != null && includeSuper) {
|
||||||
|
IMethodBinding methodBinding = findMethodBinding(binding, methodName);
|
||||||
|
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
|
||||||
|
CompilationUnit declaringCu = classes.get(methodBinding.getDeclaringClass().getErasure().getQualifiedName());
|
||||||
|
if (declaringCu != null) {
|
||||||
|
ASTNode declNode = declaringCu.findDeclaringNode(methodBinding.getKey());
|
||||||
|
if (declNode instanceof MethodDeclaration) {
|
||||||
|
return (MethodDeclaration) declNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findMethodDeclarationLegacy(td, methodName, includeSuper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IMethodBinding findMethodBinding(ITypeBinding typeBinding, String methodName) {
|
||||||
|
return findMethodBinding(typeBinding, methodName, new java.util.HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IMethodBinding findMethodBinding(ITypeBinding typeBinding, String methodName, Set<String> visited) {
|
||||||
|
if (typeBinding == null) return null;
|
||||||
|
String key = typeBinding.getKey();
|
||||||
|
if (key != null && !visited.add(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (IMethodBinding mb : typeBinding.getDeclaredMethods()) {
|
||||||
|
if (mb.getName().equals(methodName)) {
|
||||||
|
return mb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ITypeBinding superclass = typeBinding.getSuperclass();
|
||||||
|
if (superclass != null) {
|
||||||
|
IMethodBinding mb = findMethodBinding(superclass, methodName, visited);
|
||||||
|
if (mb != null) return mb;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ITypeBinding intf : typeBinding.getInterfaces()) {
|
||||||
|
IMethodBinding mb = findMethodBinding(intf, methodName, visited);
|
||||||
|
if (mb != null) return mb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findMethodDeclarationLegacy(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||||
|
if (td == null) return null;
|
||||||
for (MethodDeclaration method : td.getMethods()) {
|
for (MethodDeclaration method : td.getMethods()) {
|
||||||
if (method.getName().getIdentifier().equals(methodName)) {
|
if (method.getName().getIdentifier().equals(methodName)) {
|
||||||
return method;
|
return method;
|
||||||
@@ -525,7 +563,7 @@ public class CodebaseContext {
|
|||||||
if (superFqn != null) {
|
if (superFqn != null) {
|
||||||
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
||||||
if (superTd != null) {
|
if (superTd != null) {
|
||||||
return findMethodDeclaration(superTd, methodName, true);
|
return findMethodDeclarationLegacy(superTd, methodName, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,9 +571,9 @@ public class CodebaseContext {
|
|||||||
for (Object interfaceTypeObj : td.superInterfaceTypes()) {
|
for (Object interfaceTypeObj : td.superInterfaceTypes()) {
|
||||||
Type interfaceType = (Type) interfaceTypeObj;
|
Type interfaceType = (Type) interfaceTypeObj;
|
||||||
String interfaceName = extractTypeName(interfaceType);
|
String interfaceName = extractTypeName(interfaceType);
|
||||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (CompilationUnit) td.getRoot());
|
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null);
|
||||||
if (interfaceTd != null) {
|
if (interfaceTd != null) {
|
||||||
MethodDeclaration found = findMethodDeclaration(interfaceTd, methodName, true);
|
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true);
|
||||||
if (found != null) return found;
|
if (found != null) return found;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,15 +581,107 @@ public class CodebaseContext {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFqn(TypeDeclaration td) {
|
public AbstractTypeDeclaration getAbstractTypeDeclaration(String name) {
|
||||||
StringBuilder sb = new StringBuilder(td.getName().getIdentifier());
|
// Try exact FQN match first
|
||||||
ASTNode current = td.getParent();
|
CompilationUnit cu = classes.get(name);
|
||||||
while (current instanceof TypeDeclaration parent) {
|
if (cu != null)
|
||||||
|
return findAbstractTypeInCu(cu, name);
|
||||||
|
|
||||||
|
// If it's a simple name, check if it's ambiguous
|
||||||
|
if (ambiguousSimpleNames.contains(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not ambiguous, return the one we found during scan
|
||||||
|
String fqn = simpleNameToFqn.get(name);
|
||||||
|
if (fqn != null) {
|
||||||
|
cu = classes.get(fqn);
|
||||||
|
if (cu != null)
|
||||||
|
return findAbstractTypeInCu(cu, fqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbstractTypeDeclaration getAbstractTypeDeclaration(String name, CompilationUnit contextCu) {
|
||||||
|
if (name == null || name.isEmpty())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// 1. Check if it's already an FQN
|
||||||
|
if (classes.containsKey(name))
|
||||||
|
return getAbstractTypeDeclaration(name);
|
||||||
|
|
||||||
|
// 2. Check imports in contextCu
|
||||||
|
if (contextCu != null) {
|
||||||
|
for (Object impObj : contextCu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (!imp.isStatic() && !imp.isOnDemand()) {
|
||||||
|
if (impName.endsWith("." + name))
|
||||||
|
return getAbstractTypeDeclaration(impName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check same package
|
||||||
|
String packageName = contextCu.getPackage() != null ? contextCu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
|
String localFqn = packageName.isEmpty() ? name : packageName + "." + name;
|
||||||
|
if (classes.containsKey(localFqn))
|
||||||
|
return getAbstractTypeDeclaration(localFqn);
|
||||||
|
|
||||||
|
// 4. Check on-demand imports (star imports)
|
||||||
|
for (Object impObj : contextCu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
if (imp.isOnDemand()) {
|
||||||
|
String starFqn = imp.getName().getFullyQualifiedName() + "." + name;
|
||||||
|
if (classes.containsKey(starFqn))
|
||||||
|
return getAbstractTypeDeclaration(starFqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Fallback to java.lang (common)
|
||||||
|
String langFqn = "java.lang." + name;
|
||||||
|
if (classes.containsKey(langFqn))
|
||||||
|
return getAbstractTypeDeclaration(langFqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Last resort: global simple name match
|
||||||
|
return getAbstractTypeDeclaration(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AbstractTypeDeclaration findAbstractTypeInCu(CompilationUnit cu, String fqn) {
|
||||||
|
for (Object type : cu.types()) {
|
||||||
|
if (type instanceof AbstractTypeDeclaration atd) {
|
||||||
|
AbstractTypeDeclaration found = findNestedAbstractType(atd, fqn);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AbstractTypeDeclaration findNestedAbstractType(AbstractTypeDeclaration atd, String targetFqn) {
|
||||||
|
if (getFqn(atd).equals(targetFqn)) return atd;
|
||||||
|
if (atd instanceof TypeDeclaration td) {
|
||||||
|
for (TypeDeclaration nested : td.getTypes()) {
|
||||||
|
AbstractTypeDeclaration found = findNestedAbstractType(nested, targetFqn);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFqn(AbstractTypeDeclaration atd) {
|
||||||
|
if (atd == null) return null;
|
||||||
|
StringBuilder sb = new StringBuilder(atd.getName().getIdentifier());
|
||||||
|
ASTNode current = atd.getParent();
|
||||||
|
while (current instanceof AbstractTypeDeclaration parent) {
|
||||||
sb.insert(0, parent.getName().getIdentifier() + ".");
|
sb.insert(0, parent.getName().getIdentifier() + ".");
|
||||||
current = parent.getParent();
|
current = parent.getParent();
|
||||||
}
|
}
|
||||||
|
|
||||||
CompilationUnit cu = (CompilationUnit) td.getRoot();
|
CompilationUnit cu = (atd.getRoot() instanceof CompilationUnit) ? (CompilationUnit) atd.getRoot() : null;
|
||||||
|
if (cu == null) {
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
String fqn = sb.toString();
|
String fqn = sb.toString();
|
||||||
return packageName.isEmpty() ? fqn : packageName + "." + fqn;
|
return packageName.isEmpty() ? fqn : packageName + "." + fqn;
|
||||||
@@ -625,10 +755,19 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
||||||
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
|
ITypeBinding binding = td.resolveBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
boolean isSubtype = BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.builders.StateMachineConfigurer")
|
||||||
|
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.StateMachineConfigurerAdapter")
|
||||||
|
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter");
|
||||||
|
if (isSubtype) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extendsStateMachineConfigurerAdapterLegacy(td, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td, Set<String> visited) {
|
private boolean extendsStateMachineConfigurerAdapterLegacy(TypeDeclaration td, Set<String> visited) {
|
||||||
String fqn = getFqn(td);
|
String fqn = getFqn(td);
|
||||||
if (visited.contains(fqn)) {
|
if (visited.contains(fqn)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -645,7 +784,7 @@ public class CodebaseContext {
|
|||||||
if (knownAdapters.contains(superclassName))
|
if (knownAdapters.contains(superclassName))
|
||||||
return true;
|
return true;
|
||||||
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
||||||
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd, visited))
|
if (superTd != null && extendsStateMachineConfigurerAdapterLegacy(superTd, visited))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +795,7 @@ public class CodebaseContext {
|
|||||||
if (knownAdapters.contains(interfaceName))
|
if (knownAdapters.contains(interfaceName))
|
||||||
return true;
|
return true;
|
||||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
||||||
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd, visited))
|
if (interfaceTd != null && extendsStateMachineConfigurerAdapterLegacy(interfaceTd, visited))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class ControlFlowGraph {
|
||||||
|
private final MethodDeclaration method;
|
||||||
|
private final CfgNode entryNode = new CfgNode(null, CfgNodeType.ENTRY);
|
||||||
|
private final CfgNode exitNode = new CfgNode(null, CfgNodeType.EXIT);
|
||||||
|
private final List<CfgNode> nodes = new ArrayList<>();
|
||||||
|
|
||||||
|
public enum CfgNodeType {
|
||||||
|
ENTRY,
|
||||||
|
EXIT,
|
||||||
|
STATEMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CfgNode {
|
||||||
|
private final ASTNode astNode;
|
||||||
|
private final CfgNodeType type;
|
||||||
|
private final Set<CfgNode> predecessors = new HashSet<>();
|
||||||
|
private final Set<CfgNode> successors = new HashSet<>();
|
||||||
|
|
||||||
|
public CfgNode(ASTNode astNode, CfgNodeType type) {
|
||||||
|
this.astNode = astNode;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ASTNode getAstNode() {
|
||||||
|
return astNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CfgNodeType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<CfgNode> getPredecessors() {
|
||||||
|
return predecessors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<CfgNode> getSuccessors() {
|
||||||
|
return successors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSuccessor(CfgNode succ) {
|
||||||
|
if (succ != null) {
|
||||||
|
this.successors.add(succ);
|
||||||
|
succ.predecessors.add(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControlFlowGraph(MethodDeclaration method) {
|
||||||
|
this.method = method;
|
||||||
|
this.nodes.add(entryNode);
|
||||||
|
this.nodes.add(exitNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodDeclaration getMethod() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CfgNode getEntryNode() {
|
||||||
|
return entryNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CfgNode getExitNode() {
|
||||||
|
return exitNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CfgNode> getNodes() {
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNode(CfgNode node) {
|
||||||
|
this.nodes.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface DataFlowModel {
|
||||||
|
/**
|
||||||
|
* Resolves all possible Expressions that define the value of the given expression
|
||||||
|
* at its location in the control flow.
|
||||||
|
*/
|
||||||
|
List<Expression> getReachingDefinitions(Expression expr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the single constant/literal value if all definitions converge,
|
||||||
|
* or if a single value is expected.
|
||||||
|
*/
|
||||||
|
String resolveValue(Expression expr, CodebaseContext context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,898 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class JdtDataFlowModel implements DataFlowModel {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
|
||||||
|
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
||||||
|
private final Map<ASTNode, Map<IVariableBinding, Expression>> objectFieldBindings = new HashMap<>();
|
||||||
|
|
||||||
|
public JdtDataFlowModel(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode current = node;
|
||||||
|
while (current != null) {
|
||||||
|
if (current instanceof MethodDeclaration) {
|
||||||
|
return (MethodDeclaration) current;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReachingDefinitions getReachingDefinitionsForMethod(MethodDeclaration method) {
|
||||||
|
if (method == null) return null;
|
||||||
|
return rdCache.computeIfAbsent(method, m -> {
|
||||||
|
ControlFlowGraph cfg = cfgCache.computeIfAbsent(m, CfgBuilder::build);
|
||||||
|
return new ReachingDefinitions(cfg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Expression> getReachingDefinitions(Expression expr) {
|
||||||
|
return getReachingDefinitions(expr, new HashSet<>(), new HashMap<>(), new HashMap<>(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Expression> getReachingDefinitions(
|
||||||
|
Expression expr,
|
||||||
|
Set<ASTNode> visited,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||||
|
int depth) {
|
||||||
|
if (expr == null || depth > 50 || !visited.add(expr)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Handle Ternary / Conditional Expression
|
||||||
|
if (expr instanceof ConditionalExpression ce) {
|
||||||
|
String condVal = resolveValue(ce.getExpression(), context);
|
||||||
|
if ("true".equals(condVal)) {
|
||||||
|
List<Expression> resolved = getReachingDefinitions(ce.getThenExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
} else if ("false".equals(condVal)) {
|
||||||
|
List<Expression> resolved = getReachingDefinitions(ce.getElseExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
} else {
|
||||||
|
visited.remove(expr);
|
||||||
|
return List.of(ce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Handle Field constant / field initializer propagation
|
||||||
|
IVariableBinding vb = getVariableBinding(expr);
|
||||||
|
if (vb != null && vb.isField()) {
|
||||||
|
if (instanceFieldBindings.containsKey(vb)) {
|
||||||
|
Expression fieldVal = instanceFieldBindings.get(vb);
|
||||||
|
List<Expression> resolved = getReachingDefinitions(fieldVal, visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression fieldInit = findFieldInitializer(vb, expr);
|
||||||
|
if (fieldInit != null) {
|
||||||
|
List<Expression> resolved = getReachingDefinitions(fieldInit, visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Handle Parameter mapping for SimpleName
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
IBinding b = sn.resolveBinding();
|
||||||
|
if (b instanceof IVariableBinding paramVb && paramBindings.containsKey(paramVb)) {
|
||||||
|
Expression argExpr = paramBindings.get(paramVb);
|
||||||
|
List<Expression> resolved = getReachingDefinitions(argExpr, visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Local Reaching Definitions
|
||||||
|
MethodDeclaration md = findEnclosingMethod(sn);
|
||||||
|
ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
|
||||||
|
if (rd != null) {
|
||||||
|
Set<ASTNode> defs = rd.getReachingDefinitions(sn, sn.getIdentifier());
|
||||||
|
if (!defs.isEmpty()) {
|
||||||
|
List<Expression> exprs = new ArrayList<>();
|
||||||
|
for (ASTNode def : defs) {
|
||||||
|
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
|
||||||
|
if (defExpr != null) {
|
||||||
|
exprs.addAll(getReachingDefinitions(defExpr, visited, paramBindings, instanceFieldBindings, depth + 1));
|
||||||
|
} else {
|
||||||
|
exprs.add(expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited.remove(expr);
|
||||||
|
return exprs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ClassInstanceCreation
|
||||||
|
if (expr instanceof ClassInstanceCreation cic) {
|
||||||
|
getOrCreateFieldBindings(cic, paramBindings, instanceFieldBindings);
|
||||||
|
visited.remove(expr);
|
||||||
|
return List.of(cic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle CastExpression
|
||||||
|
if (expr instanceof CastExpression ce) {
|
||||||
|
List<Expression> resolved = getReachingDefinitions(ce.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ParenthesizedExpression
|
||||||
|
if (expr instanceof ParenthesizedExpression pe) {
|
||||||
|
List<Expression> 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) {
|
||||||
|
String mName = mi.getName().getIdentifier();
|
||||||
|
if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) {
|
||||||
|
List<Expression> resolved = getReachingDefinitions((Expression) mi.arguments().get(0), visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
IMethodBinding mb = mi.resolveMethodBinding();
|
||||||
|
if (mb != null) {
|
||||||
|
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
|
if (!targets.isEmpty()) {
|
||||||
|
List<Expression> results = new ArrayList<>();
|
||||||
|
for (TargetMethod target : targets) {
|
||||||
|
MethodDeclaration md = target.declaration;
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
Map<IVariableBinding, Expression> newParamBindings = new HashMap<>(paramBindings);
|
||||||
|
List<?> parameters = md.parameters();
|
||||||
|
List<?> arguments = mi.arguments();
|
||||||
|
int count = Math.min(parameters.size(), arguments.size());
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i);
|
||||||
|
IVariableBinding paramVb = svd.resolveBinding();
|
||||||
|
if (paramVb != null) {
|
||||||
|
newParamBindings.put(paramVb, (Expression) arguments.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate mutator/setter method body
|
||||||
|
evaluateMethodMutations(md, mi.arguments(), target.fieldBindings);
|
||||||
|
|
||||||
|
List<ReturnStatement> returns = findReturnStatements(md.getBody());
|
||||||
|
if (!returns.isEmpty()) {
|
||||||
|
for (ReturnStatement rs : returns) {
|
||||||
|
if (rs.getExpression() != null) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<IVariableBinding, Expression> newParamBindings = new HashMap<>(paramBindings);
|
||||||
|
List<?> parameters = md.parameters();
|
||||||
|
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);
|
||||||
|
IVariableBinding paramVb = svd.resolveBinding();
|
||||||
|
if (paramVb != null) {
|
||||||
|
newParamBindings.put(paramVb, (Expression) arguments.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ReturnStatement> returns = findReturnStatements(md.getBody());
|
||||||
|
if (!returns.isEmpty()) {
|
||||||
|
List<Expression> results = new ArrayList<>();
|
||||||
|
for (ReturnStatement rs : returns) {
|
||||||
|
if (rs.getExpression() != null) {
|
||||||
|
results.addAll(getReachingDefinitions(rs.getExpression(), visited, newParamBindings, instanceFieldBindings, depth + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited.remove(expr);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.remove(expr);
|
||||||
|
return List.of(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IVariableBinding getVariableBinding(Expression expr) {
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
IBinding b = sn.resolveBinding();
|
||||||
|
if (b instanceof IVariableBinding varBinding) return varBinding;
|
||||||
|
} else if (expr instanceof QualifiedName qn) {
|
||||||
|
IBinding b = qn.resolveBinding();
|
||||||
|
if (b instanceof IVariableBinding varBinding) return varBinding;
|
||||||
|
} else if (expr instanceof FieldAccess fa) {
|
||||||
|
return fa.resolveFieldBinding();
|
||||||
|
} else if (expr instanceof SuperFieldAccess sfa) {
|
||||||
|
return sfa.resolveFieldBinding();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression findFieldInitializer(IVariableBinding vb, ASTNode contextNode) {
|
||||||
|
if (vb == null) return null;
|
||||||
|
ITypeBinding declaringClass = vb.getDeclaringClass();
|
||||||
|
if (declaringClass == null) return null;
|
||||||
|
String classFqn = declaringClass.getErasure().getQualifiedName();
|
||||||
|
if (classFqn == null || classFqn.isEmpty()) return null;
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(classFqn);
|
||||||
|
if (td == null) {
|
||||||
|
CompilationUnit cu = null;
|
||||||
|
ASTNode root = contextNode.getRoot();
|
||||||
|
if (root instanceof CompilationUnit) {
|
||||||
|
cu = (CompilationUnit) root;
|
||||||
|
}
|
||||||
|
td = context.getTypeDeclaration(classFqn, cu);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td != null) {
|
||||||
|
// 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())) {
|
||||||
|
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<IVariableBinding, Expression> getOrCreateFieldBindings(
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> instanceFieldBindings) {
|
||||||
|
Map<IVariableBinding, Expression> bindings = objectFieldBindings.get(cic);
|
||||||
|
if (bindings == null) {
|
||||||
|
bindings = buildFieldBindingsFromCic(cic, paramBindings, instanceFieldBindings);
|
||||||
|
objectFieldBindings.put(cic, bindings);
|
||||||
|
}
|
||||||
|
return bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<IVariableBinding, Expression> getOrCreateFieldBindings(ClassInstanceCreation cic) {
|
||||||
|
Map<IVariableBinding, Expression> bindings = objectFieldBindings.get(cic);
|
||||||
|
if (bindings == null) {
|
||||||
|
bindings = buildFieldBindingsFromCic(cic);
|
||||||
|
objectFieldBindings.put(cic, bindings);
|
||||||
|
}
|
||||||
|
return bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<>();
|
||||||
|
IMethodBinding cb = cic.resolveConstructorBinding();
|
||||||
|
if (cb == null) return fieldBindings;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evaluateConstructor(
|
||||||
|
IMethodBinding cb,
|
||||||
|
List<?> arguments,
|
||||||
|
Map<IVariableBinding, Expression> callerParamValues,
|
||||||
|
Map<IVariableBinding, Expression> fieldBindings,
|
||||||
|
int depth) {
|
||||||
|
if (cb == null || depth > 10) return;
|
||||||
|
MethodDeclaration cd = findMethodDeclaration(cb);
|
||||||
|
if (cd == null || cd.getBody() == null) return;
|
||||||
|
|
||||||
|
Map<IVariableBinding, Expression> 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<IVariableBinding, Expression> 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<IVariableBinding, Expression> fieldBindings;
|
||||||
|
|
||||||
|
TargetMethod(MethodDeclaration declaration, Map<IVariableBinding, Expression> fieldBindings) {
|
||||||
|
this.declaration = declaration;
|
||||||
|
this.fieldBindings = fieldBindings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TargetMethod> resolveTargets(
|
||||||
|
MethodInvocation mi,
|
||||||
|
IMethodBinding mb,
|
||||||
|
Set<ASTNode> visited,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||||
|
int depth) {
|
||||||
|
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
boolean isThisReceiver = (receiver == null || receiver instanceof ThisExpression);
|
||||||
|
|
||||||
|
List<TargetMethod> targets = new ArrayList<>();
|
||||||
|
|
||||||
|
if (receiver != null) {
|
||||||
|
// Find concrete ClassInstanceCreation definitions in the reaching definitions path of the receiver
|
||||||
|
List<Expression> receiverDefs = getReachingDefinitions(receiver, visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
List<ClassInstanceCreation> 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<IVariableBinding, Expression> concreteFieldBindings =
|
||||||
|
getOrCreateFieldBindings(cic);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<String> 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<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
|
||||||
|
targets.add(new TargetMethod(md, targetFieldBindings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (targets.isEmpty()) {
|
||||||
|
MethodDeclaration md = findMethodDeclaration(mb);
|
||||||
|
if (md != null) {
|
||||||
|
Map<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
|
||||||
|
targets.add(new TargetMethod(md, targetFieldBindings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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<String> visited = new HashSet<>();
|
||||||
|
Queue<ITypeBinding> 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;
|
||||||
|
return findMethodDeclarationInType(mb.getDeclaringClass(), mb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ReturnStatement> findReturnStatements(ASTNode node) {
|
||||||
|
List<ReturnStatement> returns = new ArrayList<>();
|
||||||
|
node.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement rs) {
|
||||||
|
returns.add(rs);
|
||||||
|
return super.visit(rs);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(LambdaExpression le) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(AnonymousClassDeclaration acd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(TypeDeclarationStatement tds) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return returns;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||||
|
if (expr == null) return null;
|
||||||
|
|
||||||
|
List<Expression> defs = getReachingDefinitions(expr);
|
||||||
|
if (defs.isEmpty()) {
|
||||||
|
return context.resolveExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
String firstVal = null;
|
||||||
|
for (Expression def : defs) {
|
||||||
|
String val = context.resolveExpression(def);
|
||||||
|
if (val != null) {
|
||||||
|
if (firstVal == null) {
|
||||||
|
firstVal = val;
|
||||||
|
} else if (!firstVal.equals(val)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstVal != null ? firstVal : context.resolveExpression(expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LegacyDataFlowModel implements DataFlowModel {
|
||||||
|
private final VariableTracer variableTracer;
|
||||||
|
|
||||||
|
public LegacyDataFlowModel(VariableTracer variableTracer) {
|
||||||
|
this.variableTracer = variableTracer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Expression> getReachingDefinitions(Expression expr) {
|
||||||
|
if (variableTracer == null || expr == null) {
|
||||||
|
return expr != null ? List.of(expr) : List.of();
|
||||||
|
}
|
||||||
|
return variableTracer.traceVariableAll(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||||
|
if (expr == null) return null;
|
||||||
|
if (variableTracer == null) {
|
||||||
|
return context.resolveExpression(expr);
|
||||||
|
}
|
||||||
|
Expression traced = variableTracer.traceVariable(expr);
|
||||||
|
return context.resolveExpression(traced);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class ReachingDefinitions {
|
||||||
|
private final ControlFlowGraph cfg;
|
||||||
|
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> inMap = new HashMap<>();
|
||||||
|
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> outMap = new HashMap<>();
|
||||||
|
|
||||||
|
public ReachingDefinitions(ControlFlowGraph cfg) {
|
||||||
|
this.cfg = cfg;
|
||||||
|
analyze();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void analyze() {
|
||||||
|
List<ControlFlowGraph.CfgNode> cfgNodes = cfg.getNodes();
|
||||||
|
Map<ControlFlowGraph.CfgNode, ASTNode> nodeDefs = new HashMap<>();
|
||||||
|
Map<String, Set<ASTNode>> varToDefs = new HashMap<>();
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||||
|
ASTNode ast = node.getAstNode();
|
||||||
|
if (ast == null) continue;
|
||||||
|
|
||||||
|
ASTNode def = findDefinition(ast);
|
||||||
|
if (def != null) {
|
||||||
|
nodeDefs.put(node, def);
|
||||||
|
String varName = getDefinedVariableName(def);
|
||||||
|
if (varName != null) {
|
||||||
|
varToDefs.computeIfAbsent(varName, k -> new HashSet<>()).add(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||||
|
inMap.put(node, new HashSet<>());
|
||||||
|
outMap.put(node, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = true;
|
||||||
|
int iterations = 0;
|
||||||
|
int maxIterations = 5000;
|
||||||
|
while (changed && iterations < maxIterations) {
|
||||||
|
iterations++;
|
||||||
|
changed = false;
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||||
|
Set<ASTNode> newIn = new HashSet<>();
|
||||||
|
for (ControlFlowGraph.CfgNode pred : node.getPredecessors()) {
|
||||||
|
newIn.addAll(outMap.get(pred));
|
||||||
|
}
|
||||||
|
|
||||||
|
inMap.put(node, newIn);
|
||||||
|
|
||||||
|
Set<ASTNode> newOut = new HashSet<>(newIn);
|
||||||
|
ASTNode def = nodeDefs.get(node);
|
||||||
|
if (def != null) {
|
||||||
|
String varName = getDefinedVariableName(def);
|
||||||
|
if (varName != null) {
|
||||||
|
Set<ASTNode> allDefs = varToDefs.get(varName);
|
||||||
|
if (allDefs != null) {
|
||||||
|
newOut.removeAll(allDefs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newOut.add(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ASTNode> oldOut = outMap.put(node, newOut);
|
||||||
|
if (!newOut.equals(oldOut)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (iterations >= maxIterations) {
|
||||||
|
// Log warning or print to stderr
|
||||||
|
System.err.println("Warning: ReachingDefinitions analysis reached max iterations (" + maxIterations + ") and terminated early.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<ASTNode> getReachingDefinitions(ASTNode usageNode, String varName) {
|
||||||
|
ControlFlowGraph.CfgNode cfgNode = findCfgNodeForUsage(usageNode);
|
||||||
|
if (cfgNode == null) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ASTNode> reaching = new HashSet<>();
|
||||||
|
Set<ASTNode> inDefs = inMap.get(cfgNode);
|
||||||
|
if (inDefs != null) {
|
||||||
|
for (ASTNode def : inDefs) {
|
||||||
|
if (varName.equals(getDefinedVariableName(def))) {
|
||||||
|
reaching.add(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reaching;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ControlFlowGraph.CfgNode findCfgNodeForUsage(ASTNode usageNode) {
|
||||||
|
ASTNode current = usageNode;
|
||||||
|
while (current != null) {
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||||
|
if (node.getAstNode() == current) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ASTNode findDefinition(ASTNode ast) {
|
||||||
|
if (ast instanceof VariableDeclarationStatement vds) {
|
||||||
|
if (!vds.fragments().isEmpty()) {
|
||||||
|
return (ASTNode) vds.fragments().get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ast instanceof VariableDeclarationFragment) {
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
if (ast instanceof SingleVariableDeclaration) {
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
if (ast instanceof Assignment) {
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
if (ast instanceof ExpressionStatement es) {
|
||||||
|
return findDefinition(es.getExpression());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDefinedVariableName(ASTNode def) {
|
||||||
|
if (def instanceof VariableDeclarationFragment frag) {
|
||||||
|
return frag.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
if (def instanceof SingleVariableDeclaration svd) {
|
||||||
|
return svd.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
if (def instanceof Assignment assign) {
|
||||||
|
if (assign.getLeftHandSide() instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression getDefinitionExpression(ASTNode def) {
|
||||||
|
if (def instanceof VariableDeclarationFragment frag) {
|
||||||
|
return frag.getInitializer();
|
||||||
|
}
|
||||||
|
if (def instanceof Assignment assign) {
|
||||||
|
return assign.getRightHandSide();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
public class StateResolver {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
public StateResolver(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public State resolveState(Expression expr, CompilationUnit cu) {
|
||||||
|
if (expr == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// 1. Check for constants
|
||||||
|
String resolvedValue = context.getConstantResolver().resolve(expr, context);
|
||||||
|
if (resolvedValue != null) {
|
||||||
|
return State.of(expr.toString(), resolvedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check for @Value fields (SimpleName)
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
String fieldName = sn.getIdentifier();
|
||||||
|
TypeDeclaration td = findEnclosingType(sn);
|
||||||
|
if (td != null) {
|
||||||
|
String value = findValueFromField(td, fieldName);
|
||||||
|
if (value != null) {
|
||||||
|
return State.of(fieldName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expr instanceof StringLiteral sl)
|
||||||
|
return State.of(sl.getLiteralValue());
|
||||||
|
|
||||||
|
String raw = expr.toString();
|
||||||
|
if (expr instanceof QualifiedName qn) {
|
||||||
|
String full = resolveQualifiedName(qn, cu);
|
||||||
|
return State.of(raw, full);
|
||||||
|
}
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
String full = resolveSimpleName(sn, cu);
|
||||||
|
return State.of(raw, full);
|
||||||
|
}
|
||||||
|
return State.of(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findValueFromField(TypeDeclaration td, String fieldName) {
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(fieldName)) {
|
||||||
|
// Check for @Value annotation
|
||||||
|
for (Object mod : fd.modifiers()) {
|
||||||
|
if (mod instanceof Annotation ann) {
|
||||||
|
String name = ann.getTypeName().getFullyQualifiedName();
|
||||||
|
if (name.endsWith("Value")) {
|
||||||
|
return extractAnnotationValue(ann);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractAnnotationValue(Annotation ann) {
|
||||||
|
if (ann instanceof SingleMemberAnnotation sma) {
|
||||||
|
return stripQuotes(sma.getValue().toString());
|
||||||
|
} else if (ann instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if ("value".equals(pair.getName().getIdentifier())) {
|
||||||
|
return stripQuotes(pair.getValue().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripQuotes(String s) {
|
||||||
|
if (s == null) return null;
|
||||||
|
return s.replaceAll("^\"|\"$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (TypeDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
|
||||||
|
String qualifier = qn.getQualifier().toString();
|
||||||
|
String name = qn.getName().getIdentifier();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(qualifier, cu);
|
||||||
|
if (td != null)
|
||||||
|
return context.getFqn(td) + "." + name;
|
||||||
|
return qn.getFullyQualifiedName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSimpleName(SimpleName sn, CompilationUnit cu) {
|
||||||
|
String name = sn.getIdentifier();
|
||||||
|
if (cu == null)
|
||||||
|
return name;
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + name))
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ public class ExportService {
|
|||||||
new EntryPointEnricher(),
|
new EntryPointEnricher(),
|
||||||
new PropertyEnricher(),
|
new PropertyEnricher(),
|
||||||
new CallChainEnricher(),
|
new CallChainEnricher(),
|
||||||
|
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
|
||||||
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
|
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -88,4 +89,42 @@ public class ResolutionRobustnessTest {
|
|||||||
assertThat(context.getTypeDeclaration("p1.X")).isNotNull();
|
assertThat(context.getTypeDeclaration("p1.X")).isNotNull();
|
||||||
assertThat(context.getTypeDeclaration("p2.X")).isNotNull();
|
assertThat(context.getTypeDeclaration("p2.X")).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testJdtBindingsResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Base.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Base {\n" +
|
||||||
|
" public void run() {}\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
Files.writeString(comFoo.resolve("Child.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Child extends Base {\n" +
|
||||||
|
" public void execute() {\n" +
|
||||||
|
" run();\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
|
||||||
|
assertThat(childTd).isNotNull();
|
||||||
|
|
||||||
|
// Resolve the binding for Child class
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding binding = childTd.resolveBinding();
|
||||||
|
assertThat(binding).isNotNull();
|
||||||
|
assertThat(binding.getQualifiedName()).isEqualTo("com.foo.Child");
|
||||||
|
|
||||||
|
// Verify we can get the superclass binding
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding superBinding = binding.getSuperclass();
|
||||||
|
assertThat(superBinding).isNotNull();
|
||||||
|
assertThat(superBinding.getQualifiedName()).isEqualTo("com.foo.Base");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,4 +75,54 @@ class HeuristicEventMatchingEngineTest {
|
|||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchBidirectionalFqnAndShortName() {
|
||||||
|
// Case 1: smEvent is short, trigger event is FQN
|
||||||
|
Event smEvent1 = Event.of("PAY", "OrderEvents.PAY");
|
||||||
|
TriggerPoint triggerPoint1 = TriggerPoint.builder().event("com.example.OrderEvents.PAY").build();
|
||||||
|
assertThat(engine.matches(smEvent1, triggerPoint1)).isTrue();
|
||||||
|
|
||||||
|
// Case 2: smEvent is short, polymorphic event is FQN
|
||||||
|
Event smEvent2 = Event.of("PAY", "OrderEvents.PAY");
|
||||||
|
TriggerPoint triggerPoint2 = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("com.example.OrderEvents.PAY")).build();
|
||||||
|
assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchCaseInsensitive() {
|
||||||
|
Event smEvent = Event.of("PAY", "PAY");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder().event("pay").build();
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchGuardedContains() {
|
||||||
|
Event smEvent = Event.of("PAY", "PAY");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder().event("pay_order").build();
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
|
|
||||||
|
Event smEvent2 = Event.of("PAY_ORDER", "PAY_ORDER");
|
||||||
|
TriggerPoint triggerPoint2 = TriggerPoint.builder().event("pay").build();
|
||||||
|
assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchDottedEventPackageMismatch() {
|
||||||
|
Event smEvent = Event.of("CREATE", "com.example.other.OrderEvent.CREATE");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder().event("com.example.some.OrderEvent.CREATE").build();
|
||||||
|
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchPolymorphicDottedEventPackageMismatch() {
|
||||||
|
Event smEvent = Event.of("CREATE", "com.example.other.OrderEvent.CREATE");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||||
|
.event("getType()")
|
||||||
|
.polymorphicEvents(List.of("com.example.some.OrderEvent.CREATE"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine";
|
String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// They share com.acme.corp.division.project, but diverge into orders vs payments
|
// They share com.acme.corp.division.project, but diverge into orders vs payments
|
||||||
// This should be a strong mismatch, so it should return false
|
// This should be a strong mismatch, so it should return false
|
||||||
@@ -37,7 +37,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Same exact domain, should match
|
// Same exact domain, should match
|
||||||
assertTrue(result, "Same domain should be accepted");
|
assertTrue(result, "Same domain should be accepted");
|
||||||
@@ -51,7 +51,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Subpackage should match
|
// Subpackage should match
|
||||||
assertTrue(result, "Sub-packages of the same domain should be accepted");
|
assertTrue(result, "Sub-packages of the same domain should be accepted");
|
||||||
@@ -65,7 +65,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.orders.service.OrderStateMachine";
|
String machineName = "com.acme.orders.service.OrderStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Different subpackages, but they share the "order" domain term and common prefix
|
// Different subpackages, but they share the "order" domain term and common prefix
|
||||||
assertTrue(result, "Divergent packages that share a domain term should be accepted");
|
assertTrue(result, "Divergent packages that share a domain term should be accepted");
|
||||||
@@ -80,7 +80,7 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.corp.payments.PaymentStateMachine";
|
String machineName = "com.acme.corp.payments.PaymentStateMachine";
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Mismatched domains, but explicit variable targeting works
|
// Mismatched domains, but explicit variable targeting works
|
||||||
assertTrue(result, "Explicit variable target matching the machine name should be accepted");
|
assertTrue(result, "Explicit variable target matching the machine name should be accepted");
|
||||||
@@ -95,9 +95,36 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
|
|
||||||
String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order
|
String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order
|
||||||
|
|
||||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
// Explicit variable targeting completely conflicts
|
// Explicit variable targeting completely conflicts
|
||||||
assertFalse(result, "Explicit variable target mismatching the machine name should be rejected");
|
assertFalse(result, "Explicit variable target mismatching the machine name should be rejected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenericStateMachineConfigInGenericPackage() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.corp.orders.OrderController.submit()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.corp.config.StateMachineConfig";
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
|
||||||
|
// StateMachineConfig has generic prefix and is in config package. OrderController is in orders.
|
||||||
|
// There should be no domain mismatch because config/statemachine are generic.
|
||||||
|
assertTrue(result, "Generic state machine config in generic config package should not trigger mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoMismatchOnStateMachineConfigSuffix() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.methodChain(Arrays.asList("com.acme.shipping.service.OrderServiceImpl.place()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String machineName = "com.acme.shipping.config.MyStateMachineConfig";
|
||||||
|
|
||||||
|
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||||
|
assertTrue(result, "MyStateMachineConfig should not trigger mismatch because it contains StateMachineConfig");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,4 +152,334 @@ public class ConstantResolverTest {
|
|||||||
String result = resolver.resolve(returnExpr, context);
|
String result = resolver.resolve(returnExpr, context);
|
||||||
assertThat(result).isEqualTo("${app.path}");
|
assertThat(result).isEqualTo("${app.path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testConstructorAssignmentAndFieldAccess(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("EventDto.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class EventDto {\n" +
|
||||||
|
" private String eventType;\n" +
|
||||||
|
" public EventDto() {\n" +
|
||||||
|
" this.eventType = \"PAYMENT_SUCCESS\";\n" +
|
||||||
|
" }\n" +
|
||||||
|
" public String getEventType() {\n" +
|
||||||
|
" return this.eventType;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.example.EventDto");
|
||||||
|
MethodDeclaration method = td.getMethods()[1]; // getEventType
|
||||||
|
ReturnStatement rs = (ReturnStatement) method.getBody().statements().get(0);
|
||||||
|
Expression returnExpr = rs.getExpression(); // this.eventType
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(returnExpr, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("PAYMENT_SUCCESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodEvaluationWithNestedBlocksAndVars(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String process(String event) {\n" +
|
||||||
|
" String myVar = event;\n" +
|
||||||
|
" if (myVar != null) {\n" +
|
||||||
|
" return myVar;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" return \"DEFAULT\";\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.process(\"MY_EVENT\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process("MY_EVENT")
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("MY_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodShadowingReturnsParameter(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String a = \"OLD_FIELD\";\n" +
|
||||||
|
" public String process(String a) {\n" + // shadowing
|
||||||
|
" this.a = \"NEW_FIELD\";\n" +
|
||||||
|
" return a;\n" + // should return the parameter
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.process(\"PARAM_VALUE\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("PARAM_VALUE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodShadowingReturnsField(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String a = \"OLD_FIELD\";\n" +
|
||||||
|
" public String process(String a) {\n" + // shadowing
|
||||||
|
" this.a = \"NEW_FIELD_VALUE\";\n" +
|
||||||
|
" return this.a;\n" + // should return the field
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.process(\"PARAM_VALUE\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("NEW_FIELD_VALUE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodImplicitFieldAssignment(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String a = \"OLD_FIELD\";\n" +
|
||||||
|
" public String process(String p) {\n" +
|
||||||
|
" a = p;\n" + // implicit field assignment
|
||||||
|
" return this.a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.process(\"MY_EVENT\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("MY_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testZeroArgMethodEvaluation(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Logic.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Logic {\n" +
|
||||||
|
" public String a = \"MY_CONSTANT\";\n" +
|
||||||
|
" public String getEvent() {\n" +
|
||||||
|
" return this.a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Logic logic = new Logic();\n" +
|
||||||
|
" String result = logic.getEvent();\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
|
||||||
|
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.getEvent()
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo("MY_CONSTANT");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a switch method is called with a <em>known</em> constant argument, {@code evaluateMethodOutput}
|
||||||
|
* should evaluate the switch and return only the matching branch.
|
||||||
|
*
|
||||||
|
* <p>Regression guard: before the {@code SwitchStatement.visit} fix, the ASTVisitor would also descend
|
||||||
|
* into child {@code ReturnStatement} nodes after the switch was already handled, potentially overwriting
|
||||||
|
* a correct result with a wrong one.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testOldStyleSwitchResolvesCorrectBranchForKnownConstant(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Classifier.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Classifier {\n" +
|
||||||
|
" public OrderEvents classify(String q) {\n" +
|
||||||
|
" switch (q) {\n" +
|
||||||
|
" case \"a\": return OrderEvents.A8;\n" +
|
||||||
|
" case \"b\": return OrderEvents.B2;\n" +
|
||||||
|
" default: return OrderEvents.DEF;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}\n" +
|
||||||
|
"enum OrderEvents { A8, B2, DEF }");
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call() {\n" +
|
||||||
|
" Classifier c = new Classifier();\n" +
|
||||||
|
" OrderEvents r = c.classify(\"a\");\n" + // known constant → should match case "a"
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0];
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify("a")
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
// Only the matching branch should be returned, not all branches
|
||||||
|
assertThat(result).isEqualTo("OrderEvents.A8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a switch method is called with a <em>runtime</em> variable that cannot be statically resolved,
|
||||||
|
* {@code evaluateMethodOutput} must return {@code null} so the resolver falls back to the return-type
|
||||||
|
* enum set (covering all branches).
|
||||||
|
*
|
||||||
|
* <p>Regression: before the fix, {@code SwitchStatement.visit} returned {@code super.visit(ss)} (true),
|
||||||
|
* causing child {@code ReturnStatement} nodes inside the switch to fire. The first return — e.g.
|
||||||
|
* {@code return OrderEvents.A8} — was captured as if it were the only possible result, silently
|
||||||
|
* dropping the other branches. After the fix the visitor returns {@code false}, preventing child
|
||||||
|
* traversal, so the resolver correctly falls back to the full ENUM_SET.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testOldStyleSwitchWithUnknownVariableReturnsFallbackEnumSet(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path dir = tempDir.resolve("com/example");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.writeString(dir.resolve("Classifier.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Classifier {\n" +
|
||||||
|
" public OrderEvents classify(String q) {\n" +
|
||||||
|
" switch (q) {\n" +
|
||||||
|
" case \"a\": return OrderEvents.A8;\n" +
|
||||||
|
" case \"b\": return OrderEvents.B2;\n" +
|
||||||
|
" default: return OrderEvents.DEF;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}\n" +
|
||||||
|
"enum OrderEvents { A8, B2, DEF }");
|
||||||
|
Files.writeString(dir.resolve("Caller.java"),
|
||||||
|
"package com.example;\n" +
|
||||||
|
"public class Caller {\n" +
|
||||||
|
" public void call(String x) {\n" + // x is a runtime parameter — not a constant
|
||||||
|
" Classifier c = new Classifier();\n" +
|
||||||
|
" OrderEvents r = c.classify(x);\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
|
||||||
|
MethodDeclaration callMethod = callerTd.getMethods()[0];
|
||||||
|
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify(x)
|
||||||
|
|
||||||
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
|
String result = resolver.resolve(mi, context);
|
||||||
|
|
||||||
|
// Must cover ALL branches via ENUM_SET — not just the first one ("A8")
|
||||||
|
assertThat(result).startsWith("ENUM_SET:");
|
||||||
|
assertThat(result).contains("OrderEvents.A8");
|
||||||
|
assertThat(result).contains("OrderEvents.B2");
|
||||||
|
assertThat(result).contains("OrderEvents.DEF");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class AnonymousClassBugTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventThroughAnonymousClass() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class MyController {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
// Supplying the event via an anonymous class
|
||||||
|
fireEvent(new EventProvider() {
|
||||||
|
@Override
|
||||||
|
public TransitionEnum getEvent() {
|
||||||
|
return TransitionEnum.STATE_A;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireEvent(EventProvider provider) {
|
||||||
|
machine.fire(provider.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventProvider {
|
||||||
|
TransitionEnum getEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransitionEnum { STATE_A, STATE_B }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_anonymous");
|
||||||
|
Files.writeString(tempDir.resolve("Config.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.MyController")
|
||||||
|
.methodName("process")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("Poly events from anonymous class: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
|
||||||
|
// It SHOULD resolve to STATE_A by tracking the anonymous class implementation
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_A");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class AnonymousClassTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventFromAnonymousClass() throws Exception {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class AnonymousService {
|
||||||
|
private EventService service;
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
MyEvent event = new MyEvent() {
|
||||||
|
@Override
|
||||||
|
public String getEvent() {
|
||||||
|
return "ANON_EVENT";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
service.trigger(event.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class EventService {
|
||||||
|
public void trigger(String ev) {}
|
||||||
|
}
|
||||||
|
class MyEvent {
|
||||||
|
public String getEvent() { return "BASE_EVENT"; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
Files.writeString(tempDir.resolve("AnonymousService.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.EventService")
|
||||||
|
.methodName("trigger")
|
||||||
|
.sourceFile("AnonymousService.java")
|
||||||
|
.sourceModule("com.example")
|
||||||
|
.event("event.getEvent()")
|
||||||
|
.lineNumber(12)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.AnonymousService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
|
||||||
|
|
||||||
|
assertThat(result.getPolymorphicEvents()).contains("ANON_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class BuilderBugTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventFromBuilder() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class Controller {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
EventWrapper wrapper = EventWrapper.builder()
|
||||||
|
.event(TransitionEnum.STATE_X)
|
||||||
|
.build();
|
||||||
|
machine.fire(wrapper.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWrapper {
|
||||||
|
private TransitionEnum event;
|
||||||
|
public TransitionEnum getEvent() { return event; }
|
||||||
|
public static Builder builder() { return new Builder(); }
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private TransitionEnum event;
|
||||||
|
public Builder event(TransitionEnum e) { this.event = e; return this; }
|
||||||
|
public EventWrapper build() {
|
||||||
|
EventWrapper w = new EventWrapper();
|
||||||
|
w.event = this.event;
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransitionEnum { STATE_X, STATE_Y }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_builder");
|
||||||
|
Files.writeString(tempDir.resolve("Config.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.Controller")
|
||||||
|
.methodName("process")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("Poly events: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("TransitionEnum.STATE_X");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class CollectionIndexingTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventFromArrayAccess() throws Exception {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class ArrayService {
|
||||||
|
private EventService service;
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
String[] events = new String[] {"EVENT_1", "EVENT_2"};
|
||||||
|
service.trigger(events[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class EventService {
|
||||||
|
public void trigger(String ev) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
Files.writeString(tempDir.resolve("ArrayService.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.EventService")
|
||||||
|
.methodName("trigger")
|
||||||
|
.sourceFile("ArrayService.java")
|
||||||
|
.sourceModule("com.example")
|
||||||
|
.event("events[0]")
|
||||||
|
.lineNumber(7)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.ArrayService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
|
||||||
|
|
||||||
|
// When using array indexing, we should ideally extract the indexed element or at least all elements in the array
|
||||||
|
assertThat(result.getPolymorphicEvents()).contains("EVENT_1");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class EnterpriseBugsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveStaticImportAndAvoidMismatchedConstructorEnumArgs(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import static com.example.EventUtils.assertMatchingCancelEventType;
|
||||||
|
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleCancel(Cancellation.Sender sender) {
|
||||||
|
service.process(new OrderCancelledEvent(sender).getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(CommonOrderEvent event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderCancelledEvent {
|
||||||
|
private CommonOrderEvent type;
|
||||||
|
public OrderCancelledEvent(Cancellation.Sender sender) {
|
||||||
|
this.type = assertMatchingCancelEventType(sender);
|
||||||
|
}
|
||||||
|
public CommonOrderEvent getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventUtils {
|
||||||
|
public static CommonOrderEvent assertMatchingCancelEventType(Cancellation.Sender sender) {
|
||||||
|
return switch(sender) {
|
||||||
|
case registry_corp -> CommonOrderEvent.CANCELLED_BY_registry_corp;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cancellation {
|
||||||
|
public enum Sender { registry_corp }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CommonOrderEvent { CANCELLED_BY_registry_corp }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("handleCancel")
|
||||||
|
.build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("process")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
TriggerPoint resolvedTp = chains.get(0).getTriggerPoint();
|
||||||
|
|
||||||
|
// Assert Bug 3: assertMatchingCancelEventType method is resolved via static import
|
||||||
|
// Assert Bug 2: Cancellation.Sender.registry_corp is NOT extracted as an event argument
|
||||||
|
assertThat(resolvedTp.getPolymorphicEvents())
|
||||||
|
.as("Polymorphic events found: %s", resolvedTp.getPolymorphicEvents())
|
||||||
|
.contains("CommonOrderEvent.CANCELLED_BY_registry_corp")
|
||||||
|
.doesNotContain("registry_corp", "Cancellation.Sender.registry_corp");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAcceptConnectorCallChainUsingTriggerPointDomainMatch() {
|
||||||
|
HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.className("project.shop.OrderService")
|
||||||
|
.build())
|
||||||
|
.methodChain(List.of("project.enterprise.connector.org.OrgOrderControllerImpl.accept"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Assert Bug 4: Connector entry points targeting shop SM trigger point are accepted
|
||||||
|
boolean isMatched = engine.isRoutedToCorrectMachine(chain, "project.shop.OrderStateMachine", null);
|
||||||
|
assertThat(isMatched).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveClassConstantToSimpleNameWithoutFQNDuplicates(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public enum CommonOrderEvent { ACCEPTED_BY_corp }
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("CommonOrderEvent.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
List<String> resolved = engine.resolveClassConstantReturns("com.example.CommonOrderEvent.ACCEPTED_BY_corp", context, null);
|
||||||
|
|
||||||
|
// Assert Bug 6: resolves cleanly to simple name (since prefix is a known type)
|
||||||
|
assertThat(resolved).containsExactly("CommonOrderEvent.ACCEPTED_BY_corp");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchFqnsAndMismatchesOnRouting(@TempDir Path tempDir) throws IOException {
|
||||||
|
String statesSrc = """
|
||||||
|
package com.example;
|
||||||
|
public enum OrderStates { S1, S2 }
|
||||||
|
""";
|
||||||
|
String eventsSrc = """
|
||||||
|
package com.example;
|
||||||
|
public enum OrderEvents { E1, E2 }
|
||||||
|
""";
|
||||||
|
String otherEventsSrc = """
|
||||||
|
package com.example.other;
|
||||||
|
public enum OtherEvents { E1, E2 }
|
||||||
|
""";
|
||||||
|
String configSrc = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderStateMachineConfig extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<OrderStates, OrderEvents> {}
|
||||||
|
""";
|
||||||
|
|
||||||
|
Files.writeString(tempDir.resolve("OrderStates.java"), statesSrc);
|
||||||
|
Files.writeString(tempDir.resolve("OrderEvents.java"), eventsSrc);
|
||||||
|
Files.writeString(tempDir.resolve("OtherEvents.java"), otherEventsSrc);
|
||||||
|
Files.writeString(tempDir.resolve("OrderStateMachineConfig.java"), configSrc);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
|
||||||
|
|
||||||
|
// Scenario 1: Exact Event type argument match
|
||||||
|
CallChain chain1 = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.eventTypeFqn("com.example.OrderEvents")
|
||||||
|
.className("com.example.Service")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
assertThat(engine.isRoutedToCorrectMachine(chain1, "com.example.OrderStateMachineConfig", context)).isTrue();
|
||||||
|
|
||||||
|
// Scenario 2: Event type argument mismatch
|
||||||
|
CallChain chain2 = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.eventTypeFqn("com.example.other.OtherEvents")
|
||||||
|
.className("com.example.Service")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
assertThat(engine.isRoutedToCorrectMachine(chain2, "com.example.OrderStateMachineConfig", context)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,7 +89,8 @@ public class GenericEventDetectorTest {
|
|||||||
"import org.springframework.statemachine.StateMachine;\n" +
|
"import org.springframework.statemachine.StateMachine;\n" +
|
||||||
"public class MyService {\n" +
|
"public class MyService {\n" +
|
||||||
" private StateMachine<String, String> stateMachine;\n" +
|
" private StateMachine<String, String> stateMachine;\n" +
|
||||||
" public MyEnum getEvent() { return MyEnum.STATE_A; }\n" + // Pretend it returns the enum
|
" public MyEnum getEvent() { return someExternalCall(); }\n" +
|
||||||
|
" public MyEnum someExternalCall() { return null; }\n" +
|
||||||
" public void trigger() {\n" +
|
" public void trigger() {\n" +
|
||||||
" stateMachine.sendEvent(this.getEvent());\n" +
|
" stateMachine.sendEvent(this.getEvent());\n" +
|
||||||
" }\n" +
|
" }\n" +
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,315 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
|
||||||
|
class HeuristicCallGraphEngineExtendedTest {
|
||||||
|
/**
|
||||||
|
* Diagnostic: Generics, inherited processing, multi-enum constructor args,
|
||||||
|
* and internal constructor mapping via a switch method.
|
||||||
|
* <p>
|
||||||
|
* Pattern:
|
||||||
|
* 1. Abstract base class AbstractEventClass<T> holds the payload and transitionType.
|
||||||
|
* 2. Abstract base class also contains the method that actually fires the state machine.
|
||||||
|
* 3. MyEventClass<T> constructor takes multiple enums (InputEnum1, InputEnum2).
|
||||||
|
* 4. MyEventClass assigns: this.transitionType = computeTransitionType(InputEnum2).
|
||||||
|
* 5. computeTransitionType uses a switch to map InputEnum2 to TransitionEnum.
|
||||||
|
* <p>
|
||||||
|
* Root cause targeted: The engine must not eagerly grab InputEnum1.IGNORE or
|
||||||
|
* InputEnum2.SOURCE_B. It must evaluate the internal computeTransitionType()
|
||||||
|
* method during instantiation to resolve the final TransitionEnum.STATE_Y.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void shouldResolveTransitionEnumThroughGenericsAndInternalConstructorSwitchComputation() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class SystemController {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void processIncomingPayload(String rawData) {
|
||||||
|
// Creates concrete class with multiple enums and a generic payload.
|
||||||
|
// Engine MUST NOT stop at IGNORE or SOURCE_B.
|
||||||
|
MyEventClass<String> event = new MyEventClass<>(
|
||||||
|
rawData,
|
||||||
|
InputEnum1.IGNORE,
|
||||||
|
InputEnum2.SOURCE_B
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calls the inherited method that actually processes the event
|
||||||
|
event.fireEvent(machine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AbstractEventClass<T> {
|
||||||
|
protected T payload;
|
||||||
|
protected TransitionEnum transitionType;
|
||||||
|
|
||||||
|
public AbstractEventClass(T payload) {
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The method to actually process events
|
||||||
|
public void fireEvent(StateMachine machine) {
|
||||||
|
machine.fire(this.getTransitionType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransitionEnum getTransitionType() {
|
||||||
|
return this.transitionType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyEventClass<T> extends AbstractEventClass<T> {
|
||||||
|
private InputEnum1 routingFlag;
|
||||||
|
|
||||||
|
public MyEventClass(T payload, InputEnum1 flag, InputEnum2 source) {
|
||||||
|
super(payload);
|
||||||
|
this.routingFlag = flag;
|
||||||
|
|
||||||
|
// The trap: Assignment via an internal method evaluation
|
||||||
|
this.transitionType = computeTransitionType(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransitionEnum computeTransitionType(InputEnum2 source) {
|
||||||
|
return switch (source) {
|
||||||
|
case SOURCE_A -> TransitionEnum.STATE_X;
|
||||||
|
case SOURCE_B -> TransitionEnum.STATE_Y; // Expected resolution
|
||||||
|
case SOURCE_C -> TransitionEnum.STATE_Z;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InputEnum1 { IGNORE, PROCESS_ASYNC }
|
||||||
|
enum InputEnum2 { SOURCE_A, SOURCE_B, SOURCE_C }
|
||||||
|
enum TransitionEnum { STATE_X, STATE_Y, STATE_Z }
|
||||||
|
""";
|
||||||
|
|
||||||
|
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_generics_computation");
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.SystemController")
|
||||||
|
.methodName("processIncomingPayload")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
|
||||||
|
System.out.println("GENERICS & COMPUTATION polymorphicEvents: " +
|
||||||
|
chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
|
||||||
|
// The assertion to prove the bug is fixed.
|
||||||
|
// If the engine returns InputEnum2.SOURCE_B or InputEnum1.IGNORE, it fails.
|
||||||
|
// It MUST evaluate computeTransitionType() and return TransitionEnum.STATE_Y.
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("TransitionEnum.STATE_Y");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Diagnostic: Enum tracked through an overridden method and a super() call.
|
||||||
|
*
|
||||||
|
* Pattern:
|
||||||
|
* 1. EntryPoint calls an overridden method on a subclass (CustomHandler.handleEvent)
|
||||||
|
* with an initial enum (ActionType.BEGIN_CHECKOUT).
|
||||||
|
* 2. The subclass intercepts the call, does some localized logic, and delegates
|
||||||
|
* back to the parent using super.handleEvent(ActionType).
|
||||||
|
* 3. The parent class (BaseHandler) takes that enum, transforms it via a switch
|
||||||
|
* statement to a TransitionEnum, and fires the state machine.
|
||||||
|
*
|
||||||
|
* Root cause targeted: The engine must successfully resolve the `super` keyword
|
||||||
|
* to the parent class's AST node, maintain the argument mapping across the inheritance
|
||||||
|
* boundary, and evaluate the switch to get the final TransitionEnum. It must not
|
||||||
|
* get stuck in a recursive loop or eagerly return ActionType.BEGIN_CHECKOUT.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void shouldResolveMappedEnumAcrossOverriddenMethodAndSuperDelegation() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class ApiEndpoint {
|
||||||
|
private CustomHandler handler;
|
||||||
|
|
||||||
|
public void triggerCheckout() {
|
||||||
|
// Entry point passes the initial enum
|
||||||
|
handler.handleEvent(ActionType.BEGIN_CHECKOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseHandler {
|
||||||
|
protected StateMachine machine;
|
||||||
|
|
||||||
|
// The method that gets overridden, but ultimately does the work
|
||||||
|
public void handleEvent(ActionType action) {
|
||||||
|
TransitionEnum transition = switch (action) {
|
||||||
|
case BEGIN_CHECKOUT -> TransitionEnum.CHECKOUT_STARTED; // Expected
|
||||||
|
case ABORT_CHECKOUT -> TransitionEnum.CHECKOUT_CANCELLED;
|
||||||
|
};
|
||||||
|
machine.fire(transition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomHandler extends BaseHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleEvent(ActionType action) {
|
||||||
|
// Custom logic before delegation (simulated)
|
||||||
|
if (action == null) return;
|
||||||
|
|
||||||
|
// The trap: The engine must follow 'super' to BaseHandler
|
||||||
|
// and carry the 'action' argument with it.
|
||||||
|
super.handleEvent(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActionType { BEGIN_CHECKOUT, ABORT_CHECKOUT }
|
||||||
|
enum TransitionEnum { CHECKOUT_STARTED, CHECKOUT_CANCELLED }
|
||||||
|
""";
|
||||||
|
|
||||||
|
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_super_override");
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.ApiEndpoint")
|
||||||
|
.methodName("triggerCheckout")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
|
||||||
|
System.out.println("SUPER DELEGATION polymorphicEvents: " +
|
||||||
|
chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
|
||||||
|
// The assertion to prove the bug is fixed.
|
||||||
|
// It MUST NOT contain "ActionType.BEGIN_CHECKOUT".
|
||||||
|
// It MUST successfully trace through super.handleEvent() and resolve the switch.
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("TransitionEnum.CHECKOUT_STARTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveCorrectEnumWhenNamesCollide() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class OrderService {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void processOrder() {
|
||||||
|
// We use OrderEvent.PROCESS
|
||||||
|
machine.fire(OrderEvent.PROCESS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InventoryService {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void updateInventory() {
|
||||||
|
// We use InternalEvent.PROCESS which has same simple name
|
||||||
|
machine.fireInternal(InternalEvent.PROCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void wrongProcess() {
|
||||||
|
// This calls fire(OrderEvent) but with InternalEvent.PROCESS
|
||||||
|
// It should be REJECTED by type-aware heuristic matching if it matches fire(OrderEvent)
|
||||||
|
machine.fire(InternalEvent.PROCESS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
// Two overloaded methods or methods in different classes (simulated by names)
|
||||||
|
public void fire(OrderEvent event) {}
|
||||||
|
public void fireInternal(InternalEvent event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvent { PROCESS, CANCEL }
|
||||||
|
enum InternalEvent { PROCESS, AUDIT }
|
||||||
|
""";
|
||||||
|
|
||||||
|
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_collision");
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("CollisionConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
// Scenario 1: Trace OrderService.processOrder
|
||||||
|
EntryPoint entryPoint1 = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("processOrder")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger1 = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains1 = engine.findChains(List.of(entryPoint1), List.of(trigger1));
|
||||||
|
|
||||||
|
assertThat(chains1).hasSize(1);
|
||||||
|
assertThat(chains1.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("OrderEvent.PROCESS")
|
||||||
|
.doesNotContain("InternalEvent.PROCESS");
|
||||||
|
|
||||||
|
// Scenario 2: Trace InventoryService.updateInventory
|
||||||
|
EntryPoint entryPoint2 = EntryPoint.builder()
|
||||||
|
.className("com.example.InventoryService")
|
||||||
|
.methodName("updateInventory")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger2 = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fireInternal")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains2 = engine.findChains(List.of(entryPoint2), List.of(trigger2));
|
||||||
|
|
||||||
|
assertThat(chains2).hasSize(1);
|
||||||
|
assertThat(chains2.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("InternalEvent.PROCESS")
|
||||||
|
.doesNotContain("OrderEvent.PROCESS");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,957 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
|
||||||
|
class HeuristicCallGraphEngineGetterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromAlternativeGetterName() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus(DomainEvent domainEvent) {
|
||||||
|
service.process(domainEvent.get2Type());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainEvent {
|
||||||
|
public String get2Type() {
|
||||||
|
return "ALTERNATIVE_EVENT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_alt_getter");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ALTERNATIVE_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromInlineInstantiation() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new DomainEvent().getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainEvent {
|
||||||
|
public String getEvent() { return "INLINE_EVENT"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_inline_inst");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromInlineInstantiationWithArguments() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new DomainEvent("PAYLOAD_ID").getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainEvent {
|
||||||
|
private String id;
|
||||||
|
public DomainEvent(String id) { this.id = id; }
|
||||||
|
public String getEvent() { return "INLINE_EVENT_WITH_ARGS"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_args");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_ARGS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromInlineInstantiationWithGenerics() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new DomainEvent<String>().getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainEvent<T> {
|
||||||
|
public String getEvent() { return "INLINE_EVENT_WITH_GENERICS"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_gen");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_GENERICS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromInlineInstantiationInheritedMethod() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new SpecificDomainEvent().getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseDomainEvent {
|
||||||
|
public String getEvent() { return "BASE_EVENT_RETURN"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpecificDomainEvent extends BaseDomainEvent {
|
||||||
|
// no getEvent here, it inherits it!
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_hier");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromInlineInstantiationWithComplexArguments() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus(Order o, String abc) {
|
||||||
|
service.process(new DomainEvent(o.getId(), abc).getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Order {
|
||||||
|
public String getId() { return "123"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainEvent {
|
||||||
|
private String id;
|
||||||
|
private String extra;
|
||||||
|
public DomainEvent(String id, String extra) { this.id = id; this.extra = extra; }
|
||||||
|
public String getEvent() { return "INLINE_EVENT_COMPLEX_ARGS"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_complex_args");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_COMPLEX_ARGS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotIncludeSubclassesForInlineInstantiation() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new BaseEvent().getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseEvent {
|
||||||
|
public String getEvent() { return "BASE_EVENT_RETURN"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubEvent extends BaseEvent {
|
||||||
|
public String getEvent() { return "SUB_EVENT_RETURN"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_subclass");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
// Only BASE_EVENT_RETURN should be found. SUB_EVENT_RETURN is a subclass and should not be included for direct instantiations
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromFieldAssignedInConstructor() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new OrderPickedUpEvent().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderPickedUpEvent {
|
||||||
|
private String type;
|
||||||
|
public OrderPickedUpEvent() {
|
||||||
|
this.type = "PICKED_UP_IN_CONSTRUCTOR";
|
||||||
|
}
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_field_const");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("PICKED_UP_IN_CONSTRUCTOR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromFieldAssignedInSuperConstructor() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new OrderCancelledEvent().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseEvent {
|
||||||
|
private String type;
|
||||||
|
public BaseEvent(String type, String otherVar) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderCancelledEvent extends BaseEvent {
|
||||||
|
public OrderCancelledEvent() {
|
||||||
|
super("CANCELLED_IN_SUPER", "someVar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_super_const");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_IN_SUPER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromFieldAssignedToAnotherConstantInConstructor() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new EventWithIndirectConstant().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventConstants {
|
||||||
|
public static final String INDIRECT_EVENT = "INDIRECT_EVENT_VALUE";
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithIndirectConstant {
|
||||||
|
private String type;
|
||||||
|
public EventWithIndirectConstant() {
|
||||||
|
this.type = EventConstants.INDIRECT_EVENT;
|
||||||
|
}
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_indirect_const");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("INDIRECT_EVENT_VALUE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromConstructorInvocation() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new EventWithThisCall().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithThisCall {
|
||||||
|
private String type;
|
||||||
|
public EventWithThisCall() {
|
||||||
|
this("THIS_CALL_CONSTANT");
|
||||||
|
}
|
||||||
|
public EventWithThisCall(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_this_const");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("THIS_CALL_CONSTANT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromDynamicMethodAssignmentInConstructor() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new OrderCancelledEvent().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderCancelledEvent {
|
||||||
|
private String cancelEventType;
|
||||||
|
public OrderCancelledEvent() {
|
||||||
|
this.cancelEventType = assertMatchingCancelEventType("some_sender");
|
||||||
|
}
|
||||||
|
private String assertMatchingCancelEventType(String sender) {
|
||||||
|
return "CANCELLED_BY_SENDER";
|
||||||
|
}
|
||||||
|
public String getType() { return cancelEventType; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_dynamic_rhs");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_BY_SENDER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromFieldDeclarationsAndInitializers() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus1() {
|
||||||
|
service.process(new EventWithFieldDecl().getType());
|
||||||
|
}
|
||||||
|
public void handleStatus2() {
|
||||||
|
service.process(new EventWithInitBlock().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithFieldDecl {
|
||||||
|
private String event = "FIELD_DECL";
|
||||||
|
public String getType() { return event; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithInitBlock {
|
||||||
|
private String event;
|
||||||
|
{
|
||||||
|
event = "INIT_BLOCK";
|
||||||
|
}
|
||||||
|
public String getType() { return event; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_field_init");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build();
|
||||||
|
EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(ep1, ep2), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
|
||||||
|
|
||||||
|
java.util.List<String> allEvents = new java.util.ArrayList<>();
|
||||||
|
chains.forEach(c -> allEvents.addAll(c.getTriggerPoint().getPolymorphicEvents()));
|
||||||
|
org.assertj.core.api.Assertions.assertThat(allEvents).containsExactlyInAnyOrder("FIELD_DECL", "INIT_BLOCK");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFallbackToScraperForComplexInlineInstantiations() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
// The AST tracer will trace EventWithConstructorArg.getType()
|
||||||
|
// It will find `this.type = typeArg`, which it cannot resolve because it's a parameter.
|
||||||
|
// The fallback scraper should catch "COMPLEX_INLINE_CONST".
|
||||||
|
service.process(new EventWithConstructorArg("COMPLEX_INLINE_CONST", 123).getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithConstructorArg {
|
||||||
|
private String type;
|
||||||
|
public EventWithConstructorArg(String typeArg, int other) {
|
||||||
|
this.type = typeArg;
|
||||||
|
}
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_scraper");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventFromConstructorAssignment() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
|
||||||
|
service.process(e.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithConstructorArg {
|
||||||
|
private String type;
|
||||||
|
public EventWithConstructorArg(String typeArg, int other) {
|
||||||
|
this.type = typeArg;
|
||||||
|
}
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_constructor_assignment");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_CONSTRUCTOR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPrioritizeSetterOverConstructor() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
|
||||||
|
e.setType("EVENT_FROM_SETTER");
|
||||||
|
service.process(e.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithConstructorArg {
|
||||||
|
private String type;
|
||||||
|
public EventWithConstructorArg(String typeArg, int other) {
|
||||||
|
this.type = typeArg;
|
||||||
|
}
|
||||||
|
public void setType(String t) { this.type = t; }
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_setter_priority");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_SETTER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventFromFieldConstructorAssignment() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
private EventWithConstructorArg e;
|
||||||
|
public OrderController() {
|
||||||
|
this.e = new EventWithConstructorArg("EVENT_FROM_FIELD", 123);
|
||||||
|
}
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(this.e.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithConstructorArg {
|
||||||
|
private String type;
|
||||||
|
public EventWithConstructorArg(String typeArg, int other) {
|
||||||
|
this.type = typeArg;
|
||||||
|
}
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_field_assignment");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
System.out.println("DEBUG CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents()));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_FIELD");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventFromLombokDataAndAllArgsConstructor() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleLombok() {
|
||||||
|
LombokEvent e = new LombokEvent("LOMBOK_EVENT", 1);
|
||||||
|
service.process(e.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
class LombokEvent {
|
||||||
|
private String event;
|
||||||
|
private int id;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_lombok");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("LOMBOK_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailHeuristicAndRequireAccurateLombokMapping() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleLombok() {
|
||||||
|
MultiEvent e = new MultiEvent("CORRECT_EVENT", "WRONG_EVENT");
|
||||||
|
service.process(e.getEventType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
class MultiEvent {
|
||||||
|
private String eventType;
|
||||||
|
private String unrelatedString;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_lombok_heuristic_break");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("CORRECT_EVENT"); // If heuristic rips both, it will fail because it contains WRONG_EVENT too!
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventsWhenFieldTransformerMethodMatchesSetterPatternButArgIsNonConstant() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private Transformer transformer;
|
||||||
|
public void processEvent(String dto) {
|
||||||
|
Event e = transformer.transform(dto);
|
||||||
|
machine.fire(e.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Transformer {
|
||||||
|
Event transform(String input);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransformerImpl implements Transformer {
|
||||||
|
public Event transform(String input) {
|
||||||
|
return new Event("ORDER_DISPATCHED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Event {
|
||||||
|
private String type;
|
||||||
|
public Event(String type) { this.type = type; }
|
||||||
|
public String getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Machine {
|
||||||
|
public void fire(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_transformer");
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.Machine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
// Must resolve through TransformerImpl.transform() → "ORDER_DISPATCHED",
|
||||||
|
// NOT return empty polymorphicEvents due to early exit on the setter match.
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("ORDER_DISPATCHED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEnumPassedThroughWrapperObjectAndGetter() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class OrderController {
|
||||||
|
private OrderMapper mapper;
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void processOrder(String dto) {
|
||||||
|
OrderEvent event = mapper.toEvent(dto);
|
||||||
|
machine.fire(event.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderMapper {
|
||||||
|
OrderEvent toEvent(String input);
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderMapperImpl implements OrderMapper {
|
||||||
|
public OrderEvent toEvent(String input) {
|
||||||
|
return new OrderEvent(States.PAYMENT_RECEIVED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderEvent {
|
||||||
|
private States type;
|
||||||
|
public OrderEvent(States type) { this.type = type; }
|
||||||
|
public States getType() { return type; } // bare field, no this.
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(States event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum States { PAYMENT_RECEIVED, CANCELLED }
|
||||||
|
""";
|
||||||
|
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_wrapper");
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrder")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("DIAGNOSTIC polymorphicEvents: " +
|
||||||
|
chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("States.PAYMENT_RECEIVED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveTransformingGetterThatMapsDtoEnumToStateMachineEnum() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class OrderController {
|
||||||
|
private OrderMapper mapper;
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void processOrder(String dto) {
|
||||||
|
OrderMsg msg = mapper.toMsg(dto);
|
||||||
|
machine.fire(msg.getSmEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderMapper {
|
||||||
|
OrderMsg toMsg(String input);
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderMapperImpl implements OrderMapper {
|
||||||
|
public OrderMsg toMsg(String input) {
|
||||||
|
return new OrderMsg(OrderType.PAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderMsg {
|
||||||
|
private OrderType type;
|
||||||
|
public OrderMsg(OrderType type) { this.type = type; }
|
||||||
|
public SMEvent getSmEvent() {
|
||||||
|
return switch (type) {
|
||||||
|
case PAY -> SMEvent.PAY_RECEIVED;
|
||||||
|
case CANCEL -> SMEvent.ORDER_CANCELLED;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(SMEvent event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderType { PAY, CANCEL }
|
||||||
|
enum SMEvent { PAY_RECEIVED, ORDER_CANCELLED }
|
||||||
|
""";
|
||||||
|
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_twoenum");
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrder")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("TWO-ENUM polymorphicEvents: " +
|
||||||
|
chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
// Must return the SM enum, NOT the DTO enum (OrderType.PAY would be wrong)
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
|
||||||
|
class HeuristicCallGraphEnginePolymorphicTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolvePolymorphicInheritanceAcrossChain() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent(RichOrderEvent domainEvent) {
|
||||||
|
doProcessOrderEvent(domainEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doProcessOrderEvent(RichOrderEvent domainEvent) {
|
||||||
|
OrderEvents eventType = assertSupportedOrderEvent(domainEvent.getType());
|
||||||
|
service.updateOrderState(eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrderEvents assertSupportedOrderEvent(OrderEvents e) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(OrderEvents event) {
|
||||||
|
// sendEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RichOrderEvent {
|
||||||
|
OrderEvents getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
class CancelOrderEvent implements RichOrderEvent {
|
||||||
|
public OrderEvents getType() { return OrderEvents.CANCELLED; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReceiveOrderEvent implements RichOrderEvent {
|
||||||
|
public OrderEvents getType() { return OrderEvents.RECEIVED; }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { CREATE, PAY, CANCELLED, RECEIVED }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_poly");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveTernaryAndStringPolymorphicReturns() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent() {
|
||||||
|
service.updateOrderState(new MysteryPayload().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(Object event) {
|
||||||
|
// sendEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MysteryPayload {
|
||||||
|
public Object getType() {
|
||||||
|
int a = 5;
|
||||||
|
if (a > 10) {
|
||||||
|
return "RAW_STRING_EVENT";
|
||||||
|
}
|
||||||
|
return a > 5 ? OrderEvents.ABCD : OrderEvents.PAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { ABCD, PAY }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_ternary_string");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.ABCD", "OrderEvents.PAY", "RAW_STRING_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolvePolymorphicInheritanceThroughDelegation() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent(BaseEvent domainEvent) {
|
||||||
|
service.updateOrderState(domainEvent.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseEvent {
|
||||||
|
OrderEvents getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AbstractEvent implements BaseEvent {
|
||||||
|
protected OrderEvents internalGetType() {
|
||||||
|
return OrderEvents.PAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConcreteEvent extends AbstractEvent {
|
||||||
|
public OrderEvents getType() {
|
||||||
|
return internalGetType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnotherConcreteEvent implements BaseEvent {
|
||||||
|
public OrderEvents getType() {
|
||||||
|
return delegateToHelper();
|
||||||
|
}
|
||||||
|
private OrderEvents delegateToHelper() {
|
||||||
|
return OrderEvents.CANCELLED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { PAY, CANCELLED }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_poly_delegation");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveMethodInheritedFromAbstractBaseClass() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent(BaseEvent domainEvent) {
|
||||||
|
service.updateOrderState(domainEvent.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseEvent {
|
||||||
|
OrderEvents getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AbstractEvent implements BaseEvent {
|
||||||
|
public OrderEvents getType() {
|
||||||
|
return OrderEvents.PAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete subclass inherits getType() but doesn't override it!
|
||||||
|
class InheritingEvent extends AbstractEvent {
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { PAY }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_inherited");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromAbstractSuperclass() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new PostAcceptanceOrderModificationsRejectedEvent().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AbstractPostAcceptanceOrderModificationsEvent {
|
||||||
|
private String event;
|
||||||
|
public AbstractPostAcceptanceOrderModificationsEvent(String event) {
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
public String getType() { return event; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostAcceptanceOrderModificationsRejectedEvent extends AbstractPostAcceptanceOrderModificationsEvent {
|
||||||
|
public PostAcceptanceOrderModificationsRejectedEvent() {
|
||||||
|
super("POST_ACCEPTANCE_REJECTED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_post_acc");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("POST_ACCEPTANCE_REJECTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveMappedTransitionEnumThroughAbstractBaseClassAndMultipleArgs() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class FlowController {
|
||||||
|
private CommandFactory factory;
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void handleRequest(String payload) {
|
||||||
|
// Creates subclass with multiple args, including intermediate enum
|
||||||
|
BaseCommand cmd = factory.buildCommand(payload);
|
||||||
|
|
||||||
|
// Fires using the mapped enum from the inherited base method
|
||||||
|
machine.fire(cmd.getTransitionEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandFactory {
|
||||||
|
public BaseCommand buildCommand(String payload) {
|
||||||
|
// Passing multiple args: String, Enum1, String, int
|
||||||
|
return new WebCommand(payload, SourceType.WEB_API, "meta_v1", 42);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseCommand {
|
||||||
|
protected String id;
|
||||||
|
protected SourceType sourceType; // The intermediate enum
|
||||||
|
protected String metadata;
|
||||||
|
|
||||||
|
public BaseCommand(String id, SourceType sourceType, String metadata) {
|
||||||
|
this.id = id;
|
||||||
|
this.sourceType = sourceType;
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tricky part: Inherited method doing the mapping
|
||||||
|
public TransitionEnum getTransitionEvent() {
|
||||||
|
return switch (this.sourceType) {
|
||||||
|
case WEB_API -> TransitionEnum.ON_WEB_START;
|
||||||
|
case MOBILE_APP -> TransitionEnum.ON_MOBILE_START;
|
||||||
|
case CRON_JOB -> TransitionEnum.ON_SYSTEM_START;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebCommand extends BaseCommand {
|
||||||
|
private int priorityLevel;
|
||||||
|
|
||||||
|
public WebCommand(String id, SourceType sourceType, String metadata, int priorityLevel) {
|
||||||
|
super(id, sourceType, metadata); // Engine must track through super()
|
||||||
|
this.priorityLevel = priorityLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SourceType { WEB_API, MOBILE_APP, CRON_JOB }
|
||||||
|
enum TransitionEnum { ON_WEB_START, ON_MOBILE_START, ON_SYSTEM_START }
|
||||||
|
""";
|
||||||
|
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_complex_inheritance");
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.FlowController")
|
||||||
|
.methodName("handleRequest")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
|
||||||
|
System.out.println("COMPLEX INHERITANCE polymorphicEvents: " +
|
||||||
|
chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
|
||||||
|
// The assertion to prove the bug is fixed.
|
||||||
|
// It MUST NOT contain "SourceType.WEB_API"
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("TransitionEnum.ON_WEB_START");
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,472 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
|
||||||
|
class HeuristicCallGraphEngineTypeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveLocalSetterAndSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderService {
|
||||||
|
public void updateOrderState(String vv) {
|
||||||
|
StateMachine sm = new StateMachine();
|
||||||
|
MysteryPayload a = new MysteryPayload();
|
||||||
|
a.setType(my_func(vv));
|
||||||
|
sm.sendEvent(a.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrderEvents my_func(String q) {
|
||||||
|
return switch (q) {
|
||||||
|
case "a" -> OrderEvents.A8;
|
||||||
|
case "b" -> OrderEvents.A133;
|
||||||
|
default -> throw new IllegalArgumentException("Unknown");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { A8, A133 }
|
||||||
|
|
||||||
|
class MysteryPayload {
|
||||||
|
private OrderEvents type;
|
||||||
|
public void setType(OrderEvents type) { this.type = type; }
|
||||||
|
public OrderEvents getType() { return type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void sendEvent(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("sendEvent")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
|
||||||
|
System.out.println("Resolved poly events: " + chain.getTriggerPoint().getPolymorphicEvents());
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveOldStyleSwitchStatementPolymorphism(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderService {
|
||||||
|
public void updateOrderState(String vv) {
|
||||||
|
StateMachine sm = new StateMachine();
|
||||||
|
sm.sendEvent(getEventFromOldSwitch(vv));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrderEvents getEventFromOldSwitch(String q) {
|
||||||
|
switch (q) {
|
||||||
|
case "a": return OrderEvents.A8;
|
||||||
|
case "b":
|
||||||
|
case "c": return OrderEvents.A133;
|
||||||
|
default: return OrderEvents.PAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { A8, A133, PAY }
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void sendEvent(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveMultiValueSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderService {
|
||||||
|
public void updateOrderState(String vv) {
|
||||||
|
StateMachine sm = new StateMachine();
|
||||||
|
sm.sendEvent(getEventMultiValue(vv));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrderEvents getEventMultiValue(String q) {
|
||||||
|
return switch (q) {
|
||||||
|
case "a", "b" -> OrderEvents.A8;
|
||||||
|
case "c" -> OrderEvents.A133;
|
||||||
|
default -> OrderEvents.PAY;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { A8, A133, PAY }
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void sendEvent(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveSwitchExpressionWithBlockAndYieldPolymorphism(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderService {
|
||||||
|
public void updateOrderState(String vv) {
|
||||||
|
StateMachine sm = new StateMachine();
|
||||||
|
sm.sendEvent(getEventWithYield(vv));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrderEvents getEventWithYield(String q) {
|
||||||
|
return switch (q) {
|
||||||
|
case "a" -> {
|
||||||
|
System.out.println("Processing a");
|
||||||
|
yield OrderEvents.A8;
|
||||||
|
}
|
||||||
|
case "b" -> { yield OrderEvents.A133; }
|
||||||
|
default -> OrderEvents.PAY;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { A8, A133, PAY }
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void sendEvent(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTraceEventsThroughMapStructInterfaceImpl() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
private OrderMapper mapper = new OrderMapperImpl();
|
||||||
|
public void processOrderEvent(String eventDtoStr) {
|
||||||
|
OrderEvent e = mapper.toEvent(eventDtoStr);
|
||||||
|
service.updateOrderState(e.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(String event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderMapper {
|
||||||
|
OrderEvent toEvent(String source);
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderMapperImpl implements OrderMapper {
|
||||||
|
@Override
|
||||||
|
public OrderEvent toEvent(String source) {
|
||||||
|
return new OrderEvent("MAPPED_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderEvent {
|
||||||
|
private String event;
|
||||||
|
public OrderEvent(String event) { this.event = event; }
|
||||||
|
public String getEvent() { return event; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_mapstruct");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("MAPPED_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTraceArrayAndCollectionAccessGracefully() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent() {
|
||||||
|
OrderEvents[] events = new OrderEvents[]{OrderEvents.PAY};
|
||||||
|
service.updateOrderState(events[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { PAY }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_array");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
// We don't dynamically resolve array indexes, but it shouldn't crash.
|
||||||
|
// It returns the array access expression as a string.
|
||||||
|
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("new OrderEvents[]{OrderEvents.PAY}[0]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus(DomainEvent domainEvent) {
|
||||||
|
service.process(domainEvent.getEventForStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainEvent {
|
||||||
|
private OrderStatus status;
|
||||||
|
public String getEventForStatus() {
|
||||||
|
switch (status) {
|
||||||
|
case NEW: return "CREATE_EVENT";
|
||||||
|
case PAID: return "PAY_EVENT";
|
||||||
|
default: return "CANCEL_EVENT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderStatus { NEW, PAID }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_switch");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("CREATE_EVENT", "PAY_EVENT", "CANCEL_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromEnumReturn() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus(DomainEvent domainEvent) {
|
||||||
|
service.process(domainEvent.getEventEnum());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainEvent {
|
||||||
|
public String getEventEnum() {
|
||||||
|
return MyEvents.ORDER_PAID.name();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MyEvents { ORDER_PAID }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_enum_return");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_PAID");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventFromArrayAndList() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import java.util.List;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleArray() {
|
||||||
|
String[] events = {"ARRAY_EVENT"};
|
||||||
|
service.process(events[0]);
|
||||||
|
}
|
||||||
|
public void handleList() {
|
||||||
|
List<String> listEvents = List.of("LIST_EVENT");
|
||||||
|
service.process(listEvents.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_array");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleArray").build();
|
||||||
|
EntryPoint entryPoint2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleList").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint1, entryPoint2), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
|
||||||
|
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
|
||||||
|
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("ARRAY_EVENT", "LIST_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEnumValueOfFromString(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class Endpoint {
|
||||||
|
private OrderService service;
|
||||||
|
public void trigger(String eventString) {
|
||||||
|
service.doSomething(eventString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void doSomething(String str) {
|
||||||
|
MyEvents event = mapToEnum(str);
|
||||||
|
sendEvent(event);
|
||||||
|
}
|
||||||
|
private MyEvents mapToEnum(String str) {
|
||||||
|
return MyEvents.valueOf(str);
|
||||||
|
}
|
||||||
|
public void sendEvent(MyEvents e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MyEvents { A, B }
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("Endpoint.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.Endpoint")
|
||||||
|
.methodName("trigger")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("sendEvent")
|
||||||
|
.event("e")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("MyEvents.A", "MyEvents.B");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
|
||||||
|
class HeuristicCallGraphEngineWrapperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUnwrapDeepMethodWrappers() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent(RichOrderEvent domainEvent) {
|
||||||
|
OrderEvents eventType = wrap3(wrap2(wrap1(domainEvent.getType())));
|
||||||
|
service.updateOrderState(eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrderEvents wrap1(OrderEvents e) { return e; }
|
||||||
|
private OrderEvents wrap2(OrderEvents e) { return e; }
|
||||||
|
private OrderEvents wrap3(OrderEvents e) { return e; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(OrderEvents event) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
class RichOrderEvent {
|
||||||
|
public OrderEvents getType() { return OrderEvents.CREATE; }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { CREATE }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_deep_wrap");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.CREATE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldProperlyUnwrapDeeplyNestedWrapperCallsForFix1() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent(MyEvent event) {
|
||||||
|
OrderEvents eventType = wrap1(wrap2(wrap3(event.getType())));
|
||||||
|
service.updateOrderState(eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrderEvents wrap1(OrderEvents e) { return e; }
|
||||||
|
private OrderEvents wrap2(OrderEvents e) { return e; }
|
||||||
|
private OrderEvents wrap3(OrderEvents e) { return e; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(OrderEvents event) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyEvent {
|
||||||
|
public OrderEvents getType() { return OrderEvents.CREATE; }
|
||||||
|
}
|
||||||
|
enum OrderEvents { CREATE }
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_nested_wrap");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("processOrderEvent").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("updateOrderState").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.CREATE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotExtractClassInstanceCreationEvenWithComplexArgsForFix2() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void createOrder(String oID) {
|
||||||
|
service.processOrderEvent(new OrderCancelledEvent(new OrderId(oID)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class OrderService {
|
||||||
|
public void processOrderEvent(OrderCancelledEvent event) { }
|
||||||
|
}
|
||||||
|
class OrderCancelledEvent {
|
||||||
|
public OrderCancelledEvent(OrderId id) {}
|
||||||
|
}
|
||||||
|
class OrderId {
|
||||||
|
public OrderId(String id) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_complex_cic");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("createOrder").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("processOrderEvent").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getEvent()).isEqualTo("new OrderCancelledEvent(new OrderId(oID))");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotCrashOnStaticMethodWrapping() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus(DomainEvent domainEvent) {
|
||||||
|
service.process(String.valueOf(domainEvent.getEvent()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainEvent {
|
||||||
|
public String getEvent() { return "STATIC_WRAP_EVENT"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_static_wrap");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotExtractConstantsFromNestedClassInstanceCreations() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleNestedConstructors() {
|
||||||
|
// The event contains a nested constructor. The heuristic should NOT extract WRONG_EVENT.
|
||||||
|
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload("WRONG_EVENT"));
|
||||||
|
service.process(e.getEventType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OuterEvent {
|
||||||
|
private String eventType;
|
||||||
|
private InnerPayload payload;
|
||||||
|
public OuterEvent(String eventType, InnerPayload payload) {
|
||||||
|
this.eventType = eventType;
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
public String getEventType() { return eventType; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class InnerPayload {
|
||||||
|
private String someData;
|
||||||
|
public InnerPayload(String someData) { this.someData = someData; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_nested_constructor");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleNestedConstructors").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("CORRECT_EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotExtractConstantsFromNestedArraysOfObjects() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleNestedArrays() {
|
||||||
|
// The event contains a nested array of objects. The heuristic should NOT extract WRONG_EVENT.
|
||||||
|
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload[] { new InnerPayload("WRONG_EVENT") });
|
||||||
|
service.process(e.getEventType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OuterEvent {
|
||||||
|
private String eventType;
|
||||||
|
private InnerPayload[] payloads;
|
||||||
|
public OuterEvent(String eventType, InnerPayload[] payloads) {
|
||||||
|
this.eventType = eventType;
|
||||||
|
this.payloads = payloads;
|
||||||
|
}
|
||||||
|
public String getEventType() { return eventType; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class InnerPayload {
|
||||||
|
private String someData;
|
||||||
|
public InnerPayload(String someData) { this.someData = someData; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_nested_array");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleNestedArrays").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("CORRECT_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class InheritedMethodBugTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventFromInheritedProtectedMethodWithoutThis() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class ChildController extends ParentController {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
// Calling inherited protected method WITHOUT 'this.' or 'super.'
|
||||||
|
machine.fire(getInheritedEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ParentController {
|
||||||
|
protected TransitionEnum getInheritedEvent() {
|
||||||
|
return TransitionEnum.STATE_P; // Protected method returns STATE_P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransitionEnum { STATE_P, STATE_Q }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_inherited");
|
||||||
|
Files.writeString(tempDir.resolve("Config.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.ChildController")
|
||||||
|
.methodName("process")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("Poly events from inherited method: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
|
||||||
|
// It SHOULD resolve to STATE_P by finding getInheritedEvent() in ParentController
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_P");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class LambdaBugTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventThroughLambda() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class MyController {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
// Supplying the event via a lambda
|
||||||
|
fireEvent(() -> TransitionEnum.STATE_L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireEvent(Supplier<TransitionEnum> eventSupplier) {
|
||||||
|
machine.fire(eventSupplier.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransitionEnum { STATE_L, STATE_M }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_lambda");
|
||||||
|
Files.writeString(tempDir.resolve("Config.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.MyController")
|
||||||
|
.methodName("process")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("Poly events from lambda: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
|
||||||
|
// It SHOULD resolve to STATE_L by tracking the lambda supplier
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_L");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,4 +119,58 @@ class LocalVariableTest {
|
|||||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
.containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCEL");
|
.containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCEL");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotUnwrapLocalDomainMethod(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent(RichOrderEvent domainEvent) {
|
||||||
|
OrderEvents resolved = validateEvent(domainEvent);
|
||||||
|
service.updateOrderState(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrderEvents validateEvent(RichOrderEvent event) {
|
||||||
|
return OrderEvents.CANCEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RichOrderEvent {
|
||||||
|
public OrderEvents getType() {
|
||||||
|
return OrderEvents.PAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderEvents { PAY, CANCEL }
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
CallChain chain = chains.get(0);
|
||||||
|
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.CANCEL");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class NewBugTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveMapLookup() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Controller {
|
||||||
|
private StateMachine machine;
|
||||||
|
private static final Map<InputEnum, TransitionEnum> MAP = Map.of(
|
||||||
|
InputEnum.IN_A, TransitionEnum.STATE_X,
|
||||||
|
InputEnum.IN_B, TransitionEnum.STATE_Y
|
||||||
|
);
|
||||||
|
|
||||||
|
public void process(InputEnum input) {
|
||||||
|
TransitionEnum trans = MAP.get(input);
|
||||||
|
machine.fire(trans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InputEnum { IN_A, IN_B }
|
||||||
|
enum TransitionEnum { STATE_X, STATE_Y }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_map");
|
||||||
|
Files.writeString(tempDir.resolve("Config.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.Controller")
|
||||||
|
.methodName("process")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("Poly events: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("TransitionEnum.STATE_X", "TransitionEnum.STATE_Y");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class OverloadedConstructorBugTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventThroughOverloadedConstructors() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class MyController {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
// Creating command via 1-arg constructor
|
||||||
|
MyCommand command = new MyCommand(TransitionEnum.STATE_Z);
|
||||||
|
machine.fire(command.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyCommand {
|
||||||
|
private final TransitionEnum event;
|
||||||
|
private final String metadata;
|
||||||
|
|
||||||
|
// 1-arg constructor calls 2-arg constructor using this(...)
|
||||||
|
public MyCommand(TransitionEnum event) {
|
||||||
|
this(event, "default_metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2-arg constructor assigns the field
|
||||||
|
public MyCommand(TransitionEnum event, String metadata) {
|
||||||
|
this.event = event;
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransitionEnum getEvent() {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransitionEnum { STATE_X, STATE_Y, STATE_Z }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_constructor");
|
||||||
|
Files.writeString(tempDir.resolve("Config.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.MyController")
|
||||||
|
.methodName("process")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("Poly events from overloaded constructor: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
|
||||||
|
// It SHOULD resolve to STATE_Z by following the 'this(...)' call in the constructor
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_Z");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PolymorphicDispatchCallGraphTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolvePolymorphicDispatchCalls(@TempDir Path tempDir) throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class OrderController {
|
||||||
|
private OrderServiceRegistry registry;
|
||||||
|
|
||||||
|
public void handleRequest(String order, String event) {
|
||||||
|
registry.getOrderService(order).processOrderEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderServiceRegistry {
|
||||||
|
public OrderService getOrderService(String order) {
|
||||||
|
return null; // dynamic lookup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderService {
|
||||||
|
void processOrderEvent(String event);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformOrderService implements OrderService {
|
||||||
|
public void processOrderEvent(String event) {
|
||||||
|
// trigger state machine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransportOrderService implements OrderService {
|
||||||
|
public void processOrderEvent(String event) {
|
||||||
|
// trigger state machine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("handleRequest")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint triggerPlatform = TriggerPoint.builder()
|
||||||
|
.className("com.example.PlatformOrderService")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint triggerTransport = TriggerPoint.builder()
|
||||||
|
.className("com.example.TransportOrderService")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chainsPlatform = builder.findChains(List.of(entryPoint), List.of(triggerPlatform));
|
||||||
|
List<CallChain> chainsTransport = builder.findChains(List.of(entryPoint), List.of(triggerTransport));
|
||||||
|
|
||||||
|
assertThat(chainsPlatform).hasSize(1);
|
||||||
|
assertThat(chainsPlatform.get(0).getMethodChain())
|
||||||
|
.containsExactly(
|
||||||
|
"com.example.OrderController.handleRequest",
|
||||||
|
"com.example.PlatformOrderService.processOrderEvent"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(chainsTransport).hasSize(1);
|
||||||
|
assertThat(chainsTransport.get(0).getMethodChain())
|
||||||
|
.containsExactly(
|
||||||
|
"com.example.OrderController.handleRequest",
|
||||||
|
"com.example.TransportOrderService.processOrderEvent"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class StaticFactoryBugTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEventThroughStaticFactory() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class MyController {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
// Creating command via static factory method
|
||||||
|
MyCommand command = MyCommand.of(TransitionEnum.STATE_W);
|
||||||
|
machine.fire(command.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyCommand {
|
||||||
|
private final TransitionEnum event;
|
||||||
|
|
||||||
|
// Private constructor
|
||||||
|
private MyCommand(TransitionEnum event) {
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static factory method
|
||||||
|
public static MyCommand of(TransitionEnum event) {
|
||||||
|
return new MyCommand(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransitionEnum getEvent() {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransitionEnum { STATE_W, STATE_X, STATE_Y, STATE_Z }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_static_factory");
|
||||||
|
Files.writeString(tempDir.resolve("Config.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.MyController")
|
||||||
|
.methodName("process")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("Poly events from static factory: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
|
||||||
|
// It SHOULD resolve to STATE_W by following the static factory and its internal instantiation
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_W");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class StreamApiTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleStreamApiChains() throws Exception {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StreamService {
|
||||||
|
private EventService service;
|
||||||
|
|
||||||
|
public void process(List<MyEvent> events) {
|
||||||
|
events.stream()
|
||||||
|
.filter(e -> e.isValid())
|
||||||
|
.map(e -> e.toEvent())
|
||||||
|
.forEach(t -> service.trigger(t.getEvent()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class EventService {
|
||||||
|
public void trigger(String ev) {}
|
||||||
|
}
|
||||||
|
class MyEvent {
|
||||||
|
public boolean isValid() { return true; }
|
||||||
|
public OrderEvent toEvent() { return new OrderEvent(); }
|
||||||
|
}
|
||||||
|
class OrderEvent {
|
||||||
|
public String getEvent() { return "STREAM_EVENT"; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
Files.writeString(tempDir.resolve("StreamService.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.EventService")
|
||||||
|
.methodName("trigger")
|
||||||
|
.sourceFile("StreamService.java")
|
||||||
|
.sourceModule("com.example")
|
||||||
|
.event("t.getEvent()")
|
||||||
|
.lineNumber(11)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.StreamService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
|
||||||
|
System.out.println("TEST RESULT: " + result);
|
||||||
|
System.out.println("POLY EVENTS: " + result.getPolymorphicEvents());
|
||||||
|
|
||||||
|
assertThat(result.getPolymorphicEvents()).isNotNull().containsExactlyInAnyOrder("STREAM_EVENT");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class SuperMethodBugTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveSuperMethodCorrectly() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
public class ChildController extends ParentController {
|
||||||
|
private StateMachine machine;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransitionEnum getEvent() {
|
||||||
|
return TransitionEnum.STATE_X; // Child returns X
|
||||||
|
}
|
||||||
|
|
||||||
|
public void process() {
|
||||||
|
// Calls super.getEvent() which returns Y, NOT X!
|
||||||
|
machine.fire(super.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParentController {
|
||||||
|
public TransitionEnum getEvent() {
|
||||||
|
return TransitionEnum.STATE_Y; // Parent returns Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransitionEnum { STATE_X, STATE_Y }
|
||||||
|
""";
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_test_super");
|
||||||
|
Files.writeString(tempDir.resolve("Config.java"), source);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
|
.className("com.example.ChildController")
|
||||||
|
.methodName("process")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
System.out.println("Poly events from super: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||||
|
// It SHOULD resolve to STATE_Y because we called super.getEvent()
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_Y");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -326,4 +326,71 @@ class AstTransitionParserTest {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExtractBeanClassBodyForExternalGuard(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tempDir) throws java.io.IOException {
|
||||||
|
String guardSource = """
|
||||||
|
package com.example;
|
||||||
|
import org.springframework.statemachine.guard.Guard;
|
||||||
|
import org.springframework.statemachine.StateContext;
|
||||||
|
public class CustomGuard implements Guard<String, String> {
|
||||||
|
@Override
|
||||||
|
public boolean evaluate(StateContext<String, String> context) {
|
||||||
|
return "OK".equals(context.getMessage().getPayload());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String configSource = """
|
||||||
|
package com.example;
|
||||||
|
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||||
|
public class TestClass {
|
||||||
|
private CustomGuard paymentGuard;
|
||||||
|
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||||
|
transitions.withExternal()
|
||||||
|
.source("CREATED")
|
||||||
|
.target("PAID")
|
||||||
|
.event("PAY")
|
||||||
|
.guard(paymentGuard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("CustomGuard.java"), guardSource);
|
||||||
|
java.nio.file.Files.writeString(tempDir.resolve("TestClass.java"), configSource);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.example.TestClass");
|
||||||
|
MethodDeclaration method = context.findMethodDeclaration(td, "configure", true);
|
||||||
|
|
||||||
|
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(1);
|
||||||
|
assertThat(transitions.getFirst().getGuard().internalLogic())
|
||||||
|
.contains("return \"OK\".equals(context.getMessage().getPayload());")
|
||||||
|
.contains("public boolean evaluate");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExtractLocalVariableLambdaGuard() {
|
||||||
|
String source = """
|
||||||
|
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||||
|
import org.springframework.statemachine.guard.Guard;
|
||||||
|
public class TestClass {
|
||||||
|
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||||
|
Guard<String, String> myLocalGuard = context -> "LOCAL_OK".equals(context.getMessage());
|
||||||
|
transitions.withExternal()
|
||||||
|
.source("CREATED")
|
||||||
|
.target("PAID")
|
||||||
|
.event("PAY")
|
||||||
|
.guard(myLocalGuard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
MethodDeclaration method = createMethodDeclaration(source);
|
||||||
|
|
||||||
|
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
|
||||||
|
|
||||||
|
assertThat(transitions).hasSize(1);
|
||||||
|
assertThat(transitions.getFirst().getGuard().internalLogic())
|
||||||
|
.contains("context -> \"LOCAL_OK\".equals(context.getMessage())");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class BindingResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBindingResolverMethods(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Base.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public interface Base {\n" +
|
||||||
|
" void run();\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
Files.writeString(comFoo.resolve("Child.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Child implements Base {\n" +
|
||||||
|
" public void run() {}\n" +
|
||||||
|
" public void execute() {\n" +
|
||||||
|
" Child c = new Child();\n" +
|
||||||
|
" c.run();\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
|
||||||
|
assertThat(childTd).isNotNull();
|
||||||
|
|
||||||
|
ITypeBinding binding = BindingResolver.resolveType(childTd);
|
||||||
|
assertThat(binding).isNotNull();
|
||||||
|
assertThat(BindingResolver.getFullyQualifiedName(binding)).isEqualTo("com.foo.Child");
|
||||||
|
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.Base")).isTrue();
|
||||||
|
assertThat(BindingResolver.isSubtypeOf(binding, "java.lang.Object")).isTrue();
|
||||||
|
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.NonExistent")).isFalse();
|
||||||
|
|
||||||
|
// Find MethodDeclaration execute
|
||||||
|
MethodDeclaration executeMethod = null;
|
||||||
|
for (MethodDeclaration md : childTd.getMethods()) {
|
||||||
|
if (md.getName().getIdentifier().equals("execute")) {
|
||||||
|
executeMethod = md;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(executeMethod).isNotNull();
|
||||||
|
|
||||||
|
// Traverse the body to find ClassInstanceCreation and MethodInvocation
|
||||||
|
class Visitor extends ASTVisitor {
|
||||||
|
ClassInstanceCreation cic = null;
|
||||||
|
MethodInvocation mi = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(ClassInstanceCreation node) {
|
||||||
|
cic = node;
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
mi = node;
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Visitor visitor = new Visitor();
|
||||||
|
executeMethod.accept(visitor);
|
||||||
|
|
||||||
|
assertThat(visitor.cic).isNotNull();
|
||||||
|
IMethodBinding constrBinding = BindingResolver.resolveConstructor(visitor.cic);
|
||||||
|
assertThat(constrBinding).isNotNull();
|
||||||
|
assertThat(constrBinding.isConstructor()).isTrue();
|
||||||
|
|
||||||
|
assertThat(visitor.mi).isNotNull();
|
||||||
|
IMethodBinding methodBinding = BindingResolver.resolveMethod(visitor.mi);
|
||||||
|
assertThat(methodBinding).isNotNull();
|
||||||
|
assertThat(methodBinding.getName()).isEqualTo("run");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class ControlFlowGraphTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSequentialAndBranchingCfg(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run(boolean flag) {\n" +
|
||||||
|
" int a = 1;\n" +
|
||||||
|
" if (flag) {\n" +
|
||||||
|
" a = 2;\n" +
|
||||||
|
" } else {\n" +
|
||||||
|
" a = 3;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" int b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
ControlFlowGraph cfg = CfgBuilder.build(md);
|
||||||
|
|
||||||
|
assertThat(cfg).isNotNull();
|
||||||
|
assertThat(cfg.getEntryNode().getSuccessors()).hasSize(1);
|
||||||
|
|
||||||
|
// Entry goes to first statement: 'int a = 1;'
|
||||||
|
ControlFlowGraph.CfgNode firstStmtNode = cfg.getEntryNode().getSuccessors().iterator().next();
|
||||||
|
assertThat(firstStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
|
||||||
|
|
||||||
|
// First stmt goes to condition of 'if (flag)'
|
||||||
|
assertThat(firstStmtNode.getSuccessors()).hasSize(1);
|
||||||
|
ControlFlowGraph.CfgNode condNode = firstStmtNode.getSuccessors().iterator().next();
|
||||||
|
assertThat(condNode.getAstNode()).isInstanceOf(SimpleName.class); // flag
|
||||||
|
|
||||||
|
// Cond node goes to two branches: 'a = 2;' and 'a = 3;'
|
||||||
|
assertThat(condNode.getSuccessors()).hasSize(2);
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode branchSuccessor : condNode.getSuccessors()) {
|
||||||
|
assertThat(branchSuccessor.getAstNode()).isInstanceOf(ExpressionStatement.class); // assignment expression statement
|
||||||
|
|
||||||
|
// Each branch successor should flow into the next sequential statement: 'int b = a;'
|
||||||
|
assertThat(branchSuccessor.getSuccessors()).hasSize(1);
|
||||||
|
ControlFlowGraph.CfgNode nextStmtNode = branchSuccessor.getSuccessors().iterator().next();
|
||||||
|
assertThat(nextStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
|
||||||
|
assertThat(nextStmtNode.getSuccessors()).contains(cfg.getExitNode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,677 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class JdtDataFlowModelTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRecursiveJdtDataFlowResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run() {\n" +
|
||||||
|
" String a = \"VAL1\";\n" +
|
||||||
|
" String b = a;\n" +
|
||||||
|
" String c = b;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
VariableDeclarationStatement cStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
|
||||||
|
VariableDeclarationFragment cFrag = (VariableDeclarationFragment) cStmt.fragments().get(0);
|
||||||
|
Expression initializer = cFrag.getInitializer(); // b
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
|
||||||
|
List<Expression> defs = model.getReachingDefinitions(initializer);
|
||||||
|
assertThat(defs).isNotEmpty();
|
||||||
|
|
||||||
|
Expression singleDef = defs.get(defs.size() - 1);
|
||||||
|
assertThat(singleDef).isInstanceOf(StringLiteral.class);
|
||||||
|
assertThat(((StringLiteral) singleDef).getLiteralValue()).isEqualTo("VAL1");
|
||||||
|
|
||||||
|
String val = model.resolveValue(initializer, context);
|
||||||
|
assertThat(val).isEqualTo("VAL1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBranchDivergenceJdtDataFlowResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run(boolean flag) {\n" +
|
||||||
|
" String a = \"SAME\";\n" +
|
||||||
|
" if (flag) {\n" +
|
||||||
|
" a = \"SAME\";\n" +
|
||||||
|
" } else {\n" +
|
||||||
|
" a = \"SAME\";\n" +
|
||||||
|
" }\n" +
|
||||||
|
" String b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
|
||||||
|
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initializer = bFrag.getInitializer(); // a
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
|
||||||
|
String val = model.resolveValue(initializer, context);
|
||||||
|
// Both branches assign "SAME", so it converges to "SAME"
|
||||||
|
assertThat(val).isEqualTo("SAME");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFieldConstantPropagation(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" private static final String MY_CONST = \"CONST_VAL\";\n" +
|
||||||
|
" public void run() {\n" +
|
||||||
|
" String a = MY_CONST;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
VariableDeclarationStatement aStmt = (VariableDeclarationStatement) md.getBody().statements().get(0);
|
||||||
|
VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0);
|
||||||
|
Expression initializer = aFrag.getInitializer();
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
String val = model.resolveValue(initializer, context);
|
||||||
|
assertThat(val).isEqualTo("CONST_VAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTernaryExpressionResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run(boolean flag) {\n" +
|
||||||
|
" String a = flag ? \"TRUE_VAL\" : \"FALSE_VAL\";\n" +
|
||||||
|
" String b = true ? \"TRUE_VAL\" : \"FALSE_VAL\";\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
|
||||||
|
// 1. Unresolvable condition (flag)
|
||||||
|
VariableDeclarationStatement aStmt = (VariableDeclarationStatement) md.getBody().statements().get(0);
|
||||||
|
VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0);
|
||||||
|
Expression initA = aFrag.getInitializer();
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
List<Expression> defsA = model.getReachingDefinitions(initA);
|
||||||
|
assertThat(defsA).hasSize(1);
|
||||||
|
assertThat(defsA.get(0)).isInstanceOf(ConditionalExpression.class);
|
||||||
|
|
||||||
|
// 2. Statically resolvable condition (true)
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initB = bFrag.getInitializer();
|
||||||
|
|
||||||
|
List<Expression> defsB = model.getReachingDefinitions(initB);
|
||||||
|
assertThat(defsB).hasSize(1);
|
||||||
|
assertThat(defsB.get(0)).isInstanceOf(StringLiteral.class);
|
||||||
|
assertThat(((StringLiteral) defsB.get(0)).getLiteralValue()).isEqualTo("TRUE_VAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStaticFactoryMethodTracking(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public static String create(String value) {\n" +
|
||||||
|
" return value;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" public void run() {\n" +
|
||||||
|
" String a = create(\"FACTORY_VAL\");\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration runMd = null;
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.getName().getIdentifier().equals("run")) {
|
||||||
|
runMd = md;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(runMd).isNotNull();
|
||||||
|
|
||||||
|
VariableDeclarationStatement aStmt = (VariableDeclarationStatement) runMd.getBody().statements().get(0);
|
||||||
|
VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0);
|
||||||
|
Expression initializer = aFrag.getInitializer();
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
String val = model.resolveValue(initializer, context);
|
||||||
|
assertThat(val).isEqualTo("FACTORY_VAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoWhileLoopFlow(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run(boolean flag) {\n" +
|
||||||
|
" int a = 1;\n" +
|
||||||
|
" do {\n" +
|
||||||
|
" a = 2;\n" +
|
||||||
|
" } while (flag);\n" +
|
||||||
|
" int b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
ControlFlowGraph cfg = CfgBuilder.build(md);
|
||||||
|
|
||||||
|
// Find the condition node (flag)
|
||||||
|
ControlFlowGraph.CfgNode condNode = null;
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||||
|
if (node.getAstNode() instanceof SimpleName sn && sn.getIdentifier().equals("flag")) {
|
||||||
|
condNode = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(condNode).isNotNull();
|
||||||
|
|
||||||
|
// The condition node should loop back to the statement inside the body (a = 2;)
|
||||||
|
boolean hasLoopBackToBody = false;
|
||||||
|
for (ControlFlowGraph.CfgNode succ : condNode.getSuccessors()) {
|
||||||
|
if (succ.getAstNode() instanceof ExpressionStatement es) {
|
||||||
|
if (es.getExpression() instanceof Assignment assign) {
|
||||||
|
if (assign.getLeftHandSide() instanceof SimpleName sn && sn.getIdentifier().equals("a")) {
|
||||||
|
hasLoopBackToBody = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(hasLoopBackToBody).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<Expression> defs = model.getReachingDefinitions(initEv);
|
||||||
|
assertThat(defs).isNotEmpty();
|
||||||
|
|
||||||
|
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<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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class LegacyDataFlowModelTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLegacyDataFlowResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run() {\n" +
|
||||||
|
" String a = \"VAL1\";\n" +
|
||||||
|
" String b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initializer = frag.getInitializer(); // 'a' reference
|
||||||
|
|
||||||
|
VariableTracer tracer = new VariableTracer(context, new ConstantResolver());
|
||||||
|
DataFlowModel model = new LegacyDataFlowModel(tracer);
|
||||||
|
|
||||||
|
List<Expression> defs = model.getReachingDefinitions(initializer);
|
||||||
|
assertThat(defs).isNotEmpty();
|
||||||
|
|
||||||
|
Expression resolvedExpr = defs.get(defs.size() - 1);
|
||||||
|
assertThat(resolvedExpr).isInstanceOf(StringLiteral.class);
|
||||||
|
assertThat(((StringLiteral) resolvedExpr).getLiteralValue()).isEqualTo("VAL1");
|
||||||
|
|
||||||
|
String val = model.resolveValue(initializer, context);
|
||||||
|
assertThat(val).isEqualTo("VAL1");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class ReachingDefinitionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReachingDefinitionsBranchMerge(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run(boolean flag) {\n" +
|
||||||
|
" int a = 1;\n" +
|
||||||
|
" if (flag) {\n" +
|
||||||
|
" a = 2;\n" +
|
||||||
|
" } else {\n" +
|
||||||
|
" a = 3;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" int b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
ControlFlowGraph cfg = CfgBuilder.build(md);
|
||||||
|
ReachingDefinitions rd = new ReachingDefinitions(cfg);
|
||||||
|
|
||||||
|
// Find usage 'a' in 'int b = a;'
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
|
||||||
|
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initializer = bFrag.getInitializer(); // simpleName 'a'
|
||||||
|
|
||||||
|
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
|
||||||
|
assertThat(defs).hasSize(2);
|
||||||
|
|
||||||
|
Set<String> values = defs.stream()
|
||||||
|
.map(ReachingDefinitions::getDefinitionExpression)
|
||||||
|
.map(expr -> {
|
||||||
|
if (expr instanceof NumberLiteral nl) {
|
||||||
|
return nl.getToken();
|
||||||
|
}
|
||||||
|
return expr.toString();
|
||||||
|
})
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
assertThat(values).containsExactlyInAnyOrder("2", "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReachingDefinitionsKilled(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run() {\n" +
|
||||||
|
" int a = 1;\n" +
|
||||||
|
" a = 2;\n" +
|
||||||
|
" int b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
ControlFlowGraph cfg = CfgBuilder.build(md);
|
||||||
|
ReachingDefinitions rd = new ReachingDefinitions(cfg);
|
||||||
|
|
||||||
|
// Find usage 'a' in 'int b = a;'
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
|
||||||
|
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initializer = bFrag.getInitializer();
|
||||||
|
|
||||||
|
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
|
||||||
|
assertThat(defs).hasSize(1);
|
||||||
|
|
||||||
|
ASTNode singleDef = defs.iterator().next();
|
||||||
|
Expression defExpr = ReachingDefinitions.getDefinitionExpression(singleDef);
|
||||||
|
assertThat(defExpr).isInstanceOf(NumberLiteral.class);
|
||||||
|
assertThat(((NumberLiteral) defExpr).getToken()).isEqualTo("2");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 16,
|
"lineNumber" : 16,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "PLACE_ORDER" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 21,
|
"lineNumber" : 21,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "CANCEL_ORDER" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18,
|
"lineNumber" : 18,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "PAY_ORDER" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -50,16 +50,6 @@
|
|||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19,
|
"lineNumber" : 19,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : null
|
||||||
}, {
|
|
||||||
"event" : "EXTERNAL_TRIGGER",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
|
||||||
"methodName" : "processSubmit",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
}, {
|
}, {
|
||||||
"event" : "SUBMIT_EVENT",
|
"event" : "SUBMIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -355,37 +345,6 @@
|
|||||||
} ]
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"callChains" : [ {
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/orders/submit",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.OrderController",
|
|
||||||
"methodName" : "submitOrder",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/orders/submit",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "EXTERNAL_TRIGGER",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
|
||||||
"methodName" : "processSubmit",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "START",
|
|
||||||
"targetState" : "PROCESSING",
|
|
||||||
"event" : "EXTERNAL_TRIGGER"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /api/orders/submit",
|
"name" : "POST /api/orders/submit",
|
||||||
@@ -408,7 +367,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20,
|
"lineNumber" : 20,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "SUBMIT_EVENT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -439,7 +398,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25,
|
"lineNumber" : 25,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "CANCEL_EVENT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -536,7 +495,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18,
|
"lineNumber" : 18,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "REACTIVE_EVENT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -574,114 +533,6 @@
|
|||||||
"targetState" : "START",
|
"targetState" : "START",
|
||||||
"event" : "AUDIT_EVENT"
|
"event" : "AUDIT_EVENT"
|
||||||
} ]
|
} ]
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-setter",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
|
|
||||||
"methodName" : "testSetter",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-setter",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-setter",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
|
|
||||||
"methodName" : "testSetter",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-setter",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-setter",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
|
|
||||||
"methodName" : "testSetter",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-setter",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-setter",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
|
|
||||||
"methodName" : "testSetter",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-setter",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -705,61 +556,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "NAMED_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/primary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testPrimary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/primary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/primary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testPrimary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/primary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -786,169 +583,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19,
|
"lineNumber" : 19,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/primary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testPrimary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/primary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/primary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testPrimary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/primary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "NAMED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/named",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testNamed",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/named",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/named",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testNamed",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/named",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/named",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testNamed",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/named",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/named",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testNamed",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/named",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -975,88 +610,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "NAMED_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/qualifier",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testQualifier",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/qualifier",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/qualifier",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testQualifier",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/qualifier",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/qualifier",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testQualifier",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/qualifier",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -1083,88 +637,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/qualifier",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testQualifier",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/qualifier",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "NAMED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/fallback",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testFallback",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/fallback",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/fallback",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testFallback",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/fallback",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -1191,142 +664,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19,
|
"lineNumber" : 19,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/fallback",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testFallback",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/fallback",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/fallback",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testFallback",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/fallback",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "NAMED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-concrete",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
|
|
||||||
"methodName" : "testConcrete",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-concrete",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-concrete",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
|
|
||||||
"methodName" : "testConcrete",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-concrete",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-concrete",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
|
|
||||||
"methodName" : "testConcrete",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-concrete",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -1353,142 +691,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-concrete",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
|
|
||||||
"methodName" : "testConcrete",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-concrete",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "NAMED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-field",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
|
|
||||||
"methodName" : "testField",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-field",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-field",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
|
|
||||||
"methodName" : "testField",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-field",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-field",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
|
|
||||||
"methodName" : "testField",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-field",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-field",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
|
|
||||||
"methodName" : "testField",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-field",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -1515,7 +718,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "NAMED_EVENT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
|
|||||||
@@ -50,16 +50,6 @@
|
|||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19,
|
"lineNumber" : 19,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : null
|
||||||
}, {
|
|
||||||
"event" : "EXTERNAL_TRIGGER",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
|
||||||
"methodName" : "processSubmit",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
}, {
|
}, {
|
||||||
"event" : "SUBMIT_EVENT",
|
"event" : "SUBMIT_EVENT",
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||||
@@ -355,37 +345,6 @@
|
|||||||
} ]
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"callChains" : [ {
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/orders/submit",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.OrderController",
|
|
||||||
"methodName" : "submitOrder",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/orders/submit",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "EXTERNAL_TRIGGER",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
|
||||||
"methodName" : "processSubmit",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : [ {
|
|
||||||
"sourceState" : "START",
|
|
||||||
"targetState" : "PROCESSING",
|
|
||||||
"event" : "EXTERNAL_TRIGGER"
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /api/orders/submit",
|
"name" : "POST /api/orders/submit",
|
||||||
@@ -408,7 +367,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 20,
|
"lineNumber" : 20,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "SUBMIT_EVENT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -439,7 +398,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25,
|
"lineNumber" : 25,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "CANCEL_EVENT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -536,7 +495,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 18,
|
"lineNumber" : 18,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "REACTIVE_EVENT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -574,114 +533,6 @@
|
|||||||
"targetState" : "START",
|
"targetState" : "START",
|
||||||
"event" : "AUDIT_EVENT"
|
"event" : "AUDIT_EVENT"
|
||||||
} ]
|
} ]
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-setter",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
|
|
||||||
"methodName" : "testSetter",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-setter",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-setter",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
|
|
||||||
"methodName" : "testSetter",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-setter",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-setter",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
|
|
||||||
"methodName" : "testSetter",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-setter",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-setter",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
|
|
||||||
"methodName" : "testSetter",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-setter",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -705,61 +556,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "NAMED_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/primary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testPrimary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/primary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/primary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testPrimary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/primary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -786,169 +583,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19,
|
"lineNumber" : 19,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/primary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testPrimary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/primary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/primary",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testPrimary",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/primary",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "NAMED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/named",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testNamed",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/named",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/named",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testNamed",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/named",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/named",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testNamed",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/named",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/named",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testNamed",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/named",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -975,88 +610,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "NAMED_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/qualifier",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testQualifier",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/qualifier",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/qualifier",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testQualifier",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/qualifier",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/qualifier",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testQualifier",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/qualifier",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -1083,88 +637,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/qualifier",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testQualifier",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/qualifier",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "NAMED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/fallback",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testFallback",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/fallback",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/fallback",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testFallback",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/fallback",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -1191,142 +664,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 19,
|
"lineNumber" : 19,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/fallback",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testFallback",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/fallback",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "POST /api/quirk/fallback",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
|
|
||||||
"methodName" : "testFallback",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/api/quirk/fallback",
|
|
||||||
"verb" : "POST"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "NAMED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-concrete",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
|
|
||||||
"methodName" : "testConcrete",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-concrete",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-concrete",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
|
|
||||||
"methodName" : "testConcrete",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-concrete",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-concrete",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
|
|
||||||
"methodName" : "testConcrete",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-concrete",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -1353,142 +691,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-concrete",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
|
|
||||||
"methodName" : "testConcrete",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-concrete",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "NAMED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-field",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
|
|
||||||
"methodName" : "testField",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-field",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.FallbackQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "FALLBACK_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-field",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
|
|
||||||
"methodName" : "testField",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-field",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PROFILED_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-field",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
|
|
||||||
"methodName" : "testField",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-field",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "PRIMARY_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 19,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
|
||||||
"contextMachineId" : null,
|
|
||||||
"matchedTransitions" : null
|
|
||||||
}, {
|
|
||||||
"entryPoint" : {
|
|
||||||
"type" : "REST",
|
|
||||||
"name" : "GET /test-field",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
|
|
||||||
"methodName" : "testField",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
|
|
||||||
"metadata" : {
|
|
||||||
"path" : "/test-field",
|
|
||||||
"verb" : "GET"
|
|
||||||
},
|
|
||||||
"parameters" : [ ]
|
|
||||||
},
|
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
|
||||||
"triggerPoint" : {
|
|
||||||
"event" : "QUALIFIER_EVENT",
|
|
||||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
|
||||||
"methodName" : "doQuirk",
|
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
|
||||||
"sourceModule" : null,
|
|
||||||
"stateMachineId" : null,
|
|
||||||
"sourceState" : null,
|
|
||||||
"lineNumber" : 17,
|
|
||||||
"polymorphicEvents" : null
|
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
@@ -1515,7 +718,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 17,
|
"lineNumber" : 17,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "NAMED_EVENT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : null
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 15,
|
"lineNumber" : 15,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "INHERITED_SUBMIT" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -338,7 +338,7 @@
|
|||||||
},
|
},
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payCast", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payCast", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||||
"triggerPoint" : {
|
"triggerPoint" : {
|
||||||
"event" : "(BaseEvent)new PayEvent()",
|
"event" : "new PayEvent()",
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||||
"methodName" : "processEvent",
|
"methodName" : "processEvent",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||||
@@ -381,10 +381,18 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 13,
|
"lineNumber" : 13,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvents.PAY", "OrderEvents.CANCEL" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderStates.SUBMITTED",
|
||||||
|
"targetState" : "OrderStates.PAID",
|
||||||
|
"event" : "OrderEvents.PAY"
|
||||||
|
}, {
|
||||||
|
"sourceState" : "OrderStates.SUBMITTED",
|
||||||
|
"targetState" : "OrderStates.CANCELED",
|
||||||
|
"event" : "OrderEvents.CANCEL"
|
||||||
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -400,7 +408,7 @@
|
|||||||
},
|
},
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||||
"triggerPoint" : {
|
"triggerPoint" : {
|
||||||
"event" : "event",
|
"event" : "new PayEvent()",
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||||
"methodName" : "processEvent",
|
"methodName" : "processEvent",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||||
@@ -408,25 +416,13 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 13,
|
"lineNumber" : 13,
|
||||||
"polymorphicEvents" : null
|
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
"sourceState" : "OrderStates.SUBMITTED",
|
||||||
"targetState" : "OrderStates.PAID",
|
"targetState" : "OrderStates.PAID",
|
||||||
"event" : "OrderEvents.PAY"
|
"event" : "OrderEvents.PAY"
|
||||||
}, {
|
|
||||||
"sourceState" : "OrderStates.PAID",
|
|
||||||
"targetState" : "OrderStates.FULFILLED",
|
|
||||||
"event" : "OrderEvents.FULFILL"
|
|
||||||
}, {
|
|
||||||
"sourceState" : "OrderStates.SUBMITTED",
|
|
||||||
"targetState" : "OrderStates.CANCELED",
|
|
||||||
"event" : "OrderEvents.CANCEL"
|
|
||||||
}, {
|
|
||||||
"sourceState" : "OrderStates.PAID",
|
|
||||||
"targetState" : "OrderStates.CANCELED",
|
|
||||||
"event" : "OrderEvents.ABCD"
|
|
||||||
} ]
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
@@ -443,7 +439,7 @@
|
|||||||
},
|
},
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderStatic", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderStatic", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||||
"triggerPoint" : {
|
"triggerPoint" : {
|
||||||
"event" : "EventBuilder.buildEvent()",
|
"event" : "new FulfillEvent()",
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||||
"methodName" : "processEvent",
|
"methodName" : "processEvent",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||||
@@ -451,10 +447,14 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 13,
|
"lineNumber" : 13,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvents.FULFILL" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderStates.PAID",
|
||||||
|
"targetState" : "OrderStates.FULFILLED",
|
||||||
|
"event" : "OrderEvents.FULFILL"
|
||||||
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
@@ -470,7 +470,7 @@
|
|||||||
},
|
},
|
||||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||||
"triggerPoint" : {
|
"triggerPoint" : {
|
||||||
"event" : "new EventBuilder().buildInstanceEvent()",
|
"event" : "new CancelEvent()",
|
||||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||||
"methodName" : "processEvent",
|
"methodName" : "processEvent",
|
||||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||||
@@ -478,10 +478,14 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 13,
|
"lineNumber" : 13,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvents.CANCEL" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "OrderStates.SUBMITTED",
|
||||||
|
"targetState" : "OrderStates.CANCELED",
|
||||||
|
"event" : "OrderEvents.CANCEL"
|
||||||
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25,
|
"lineNumber" : 25,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25,
|
"lineNumber" : 25,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25,
|
"lineNumber" : 25,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25,
|
"lineNumber" : 25,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 78,
|
"lineNumber" : 78,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -311,7 +311,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 25,
|
"lineNumber" : 25,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
@@ -346,7 +346,7 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 50,
|
"lineNumber" : 50,
|
||||||
"polymorphicEvents" : [ ]
|
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : [ {
|
"matchedTransitions" : [ {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"methodFqn": "ExternalTaxService.calculateTax",
|
|
||||||
"event": "FINALIZE"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"methodFqn": "ExternalNotificationService.sendExternalNotification",
|
|
||||||
"event": "EXTERNAL_TRIGGER"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user