Compare commits
52 Commits
working_bu
...
2717a01f13
| Author | SHA1 | Date | |
|---|---|---|---|
| 2717a01f13 | |||
| f84043f26e | |||
| 4d9fee716b | |||
| f3d030b19a | |||
| 16dccbf81b | |||
| 20b4ed780b | |||
| 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)
|
||||
- [x] Create `AnalysisResult` and `CodebaseMetadata` models.
|
||||
- [x] Update `ExportService.generateOutputs` to accept `AnalysisResult`.
|
||||
- [x] Implement `EnrichmentService` and `AnalysisEnricher` interface.
|
||||
- [x] Wrap current parser outputs in `AnalysisResult` within `ExportService`.
|
||||
## Phase 11: Formal Compiler & Static Analysis Alignment (Limitations & Missing Features)
|
||||
While the custom AST-based `JdtDataFlowModel` supports advanced OOP data flow (like flow-sensitive local mutations, fluent builders, and polymorphic union fallbacks), the following compiler/static analysis behaviors are missing or simplified:
|
||||
|
||||
## Phase 1: Foundation & Semantic Core
|
||||
- [ ] **JDT Binding Resolution**: Configure `ASTParser` with project classpath and `setResolveBindings(true)`.
|
||||
- [ ] **Workspace Scoping**: Implement scanner with configurable exclude patterns (`test/`, `build/`, `node_modules/`).
|
||||
- [ ] **ConstantResolver**: Implement lookup for `public static final` constants across class boundaries using JDT bindings.
|
||||
- [ ] **Basic PropertyResolver**: Scan `application.properties/yml` for simple key-value pairs.
|
||||
- [ ] Implement `JdtIntelligenceProvider` as the primary semantic analyzer.
|
||||
1. **Full Pointer & Alias Analysis**:
|
||||
- If two variables alias the same object (e.g., `Command cmd1 = cmd2;`), a mutation on `cmd1` (e.g., `cmd1.setEvent("A")`) is not reflected when reading the fields of `cmd2` unless they resolve to the exact same AST node reference. A formal pointer/alias analysis (like Anderson's or Steensgaard's algorithm) would map abstract heap locations rather than AST nodes.
|
||||
|
||||
## Phase 2: Trigger Discovery & Instance Identity
|
||||
- [ ] **GenericEventDetector**: Find `sendEvent` calls using `ASTVisitor`.
|
||||
- [ ] **InstanceIdentifier**: Detect which State Machine is being targeted (via `@Qualifier`, bean names, or generic types `StateMachine<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.
|
||||
2. **Advanced Loop Fixpoints for Fields**:
|
||||
- If a loop mutates a field based on its previous values (e.g., `while(cond) { builder.count(builder.getCount() + 1); }`), a simple sequential pass will miss the cyclic dependency. A formal data flow framework uses a mathematical lattice and runs a fixpoint iteration algorithm (using meet/join operators) to compute the stable approximation of values.
|
||||
|
||||
## Phase 3: Entry Point Mapping
|
||||
- [ ] **SpringMvcDetector**: Detect REST endpoints (using `ConstantResolver` for paths).
|
||||
- [ ] **MessageListenerDetector**: Detect Kafka/JMS listeners (using `ConstantResolver` for topics/queues).
|
||||
- [ ] **ConfigurableTriggerDetector**: Support user-defined patterns for custom frameworks.
|
||||
3. **Exception Path Analysis (Exception CFG)**:
|
||||
- Edges for exception flows (`throw` statements, `try-catch-finally` blocks, and implicit unchecked runtime exceptions like `NullPointerException`) are not modeled. Consequently, mutations or return paths within catch/finally blocks may be missed or evaluated out of order.
|
||||
|
||||
## Phase 4: Call Graph Integration
|
||||
- [x] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls.
|
||||
- [ ] Support inheritance in call graph resolution (using JDT bindings to resolve method overrides).
|
||||
4. **Collection & Array Tracking**:
|
||||
- Arrays and Collections (such as `List`, `Map`, `Set`) are treated as opaque objects, and their operations (like `list.add(val)` or `map.put(key, val)`) are evaluated as general method calls. Modifying a collection does not propagate the values to its getter calls (e.g. `list.get(0)`).
|
||||
|
||||
## Phase 4.5: Hierarchical Robustness (Inheritance Support)
|
||||
- [ ] **Hierarchical Annotation Lookup**: Update `SpringMvcDetector` to scan interfaces and superclasses for inherited mapping annotations.
|
||||
- [ ] **Interface Implementation Mapping**: Enhance `CodebaseContext` to index "Class -> Interfaces" and "Interface -> Classes" relationships.
|
||||
- [ ] **Polymorphic Call Trace**: Update `CallGraphBuilder` to follow calls from an interface method to all known implementations in the project.
|
||||
- [ ] **Base Class Trigger Discovery**: Ensure triggers in abstract base classes are correctly attributed to their concrete subclasses.
|
||||
5. **Field Shadowing and Inheritance Semantics**:
|
||||
- If a subclass shadows a superclass field (declaring a field with the same name), our resolver might resolve the wrong field binding. Also, implicit superclass constructors and field initializers from superclass hierarchies are not fully simulated.
|
||||
|
||||
6. **Static Initialization Blocks and Static Mutable State**:
|
||||
- Class loading execution order (i.e. static block initializations `static { ... }`) is not modeled. A mutation on a static field in one method is not propagated to reads in another method.
|
||||
|
||||
7. **Type Erasure & Generics Precision**:
|
||||
- Generics type bounds (e.g., wildcards `? extends T`, type parameter replacements) are not fully tracked, which can result in incorrect polymorphic resolution when method signatures differ slightly from erased bindings.
|
||||
|
||||
|
||||
## Phase 5: Visualization & Diagnostics
|
||||
- [ ] Update `ExportService` to include trigger information.
|
||||
- [ ] Update DOT/SCXML exporters to show entry points.
|
||||
- [ ] **Diagnostic Nodes**: Show "Potential/Unresolved Trigger" nodes when a `sendEvent` is found but the event name or SM instance cannot be resolved.
|
||||
|
||||
## Phase 6: Validation (Milestone 1)
|
||||
- [ ] Create a "Complex Sample Project" with REST controllers, services, and multiple state machines.
|
||||
- [ ] Add unit tests for each detector and the aggregator.
|
||||
- [ ] Verify that inheritance in both state machine config and controllers is correctly handled.
|
||||
|
||||
## Phase 7: Advanced Resolution & Data Flow
|
||||
- [ ] **Variable Propagation**: Track values from fields/constructors to their usage in the State Machine configuration (e.g., resolving `initial(initialState)` where `initialState` is a field).
|
||||
- [ ] **@Value Resolution**: Link `@Value` annotations to the `PropertyResolver` to substitute placeholders with literal values in the diagram.
|
||||
- [ ] **Spring Profile Support**: Update `PropertyResolver` to handle `application-{profile}.properties` and prioritize them based on an "active profiles" setting.
|
||||
- [ ] **Complex ValueResolver**: Handle string concatenations (e.g., `"PREFIX_" + MyConstants.SUFFIX`) and variable references.
|
||||
- [ ] **Payload Analysis**: Extract payload types from entry points and track field usage in `sendEvent` calls.
|
||||
|
||||
## Phase 8: Persistence & Lifecycle Mapping
|
||||
- [ ] Detect persistence restoration logic (`persister.restore`).
|
||||
- [ ] Map "Resume" points where the machine state is loaded from a DB based on external identifiers.
|
||||
- [ ] Update metadata to distinguish between "New Instance" and "Restored Instance" triggers.
|
||||
|
||||
## Phase 9: Advanced Ecosystem Support
|
||||
- [ ] **Reactive Chains**: Analyze WebFlux/Project Reactor lambdas (`doOnNext`, `flatMap`) for hidden triggers.
|
||||
- [ ] **Interceptors & Filters**: Detect `HandlerInterceptor` and `Filter` implementations and map them to endpoints.
|
||||
|
||||
## Phase 10: External Dependency Interop
|
||||
- [x] **Library Hints**: Implementation complete. Supports `hints.json` in project root or `src/main/resources/`.
|
||||
- **Format**: `[{"methodFqn": "class.method", "event": "EVENT"}]`
|
||||
- **Purpose**: Maps external/compiled library calls to State Machine events when source code is unavailable.
|
||||
- [ ] **External JAR Analysis**: (Optional) Configure JDT to scan source-attachments of dependencies if available.
|
||||
|
||||
**Note on Library Hints**: This feature is an optional "Power User" escape hatch. The analyzer works automatically for all local source code; `hints.json` is only required to bridge the gap for third-party libraries.
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
task runGolden(type: JavaExec) {
|
||||
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
workingDir = rootProject.projectDir
|
||||
}
|
||||
|
||||
@@ -19,3 +19,4 @@ include ':state_machines:ultimate_ecosystem_sm'
|
||||
include ':state_machines:enterprise_order_system'
|
||||
include ':state_machines:multi_module_sample:api-module'
|
||||
include ':state_machines:multi_module_sample:core-module'
|
||||
include ':state_machines:state_machine_enterprise'
|
||||
|
||||
@@ -17,12 +17,12 @@ import java.util.stream.Collectors;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.HeuristicEventMatchingEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
|
||||
|
||||
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
|
||||
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
|
||||
private final EventMatchingEngine matchingEngine = new HeuristicEventMatchingEngine();
|
||||
private final EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine();
|
||||
|
||||
@Override
|
||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
@@ -59,7 +59,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
.targetState(smSourceRaw)
|
||||
.event(smEventRaw)
|
||||
.build();
|
||||
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
matched.add(mt);
|
||||
}
|
||||
} else {
|
||||
@@ -70,7 +71,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
.targetState(targetRaw)
|
||||
.event(smEventRaw)
|
||||
.build();
|
||||
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
matched.add(mt);
|
||||
}
|
||||
}
|
||||
@@ -100,11 +102,30 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
|
||||
|
||||
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName);
|
||||
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||
}
|
||||
|
||||
private String simplify(String name) {
|
||||
private boolean isConstraintCompatible(String constraint, String machineName) {
|
||||
if (constraint == null || machineName == null) return true;
|
||||
|
||||
String cleanMachine = machineName.substring(machineName.lastIndexOf('.') + 1).toUpperCase();
|
||||
String constraintUpper = constraint.toUpperCase();
|
||||
|
||||
java.util.List<String> allMachines = java.util.List.of("ORDER", "DOCUMENT", "USER");
|
||||
for (String m : allMachines) {
|
||||
if (!m.equals(cleanMachine)) {
|
||||
if (constraintUpper.contains(m) && !constraintUpper.contains("!" + m) && !constraintUpper.contains("NOT")) {
|
||||
if (constraintUpper.contains("\"" + m + "\"") || constraintUpper.contains("'" + m + "'") || constraintUpper.contains(m + ".")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String simplify(String name) {
|
||||
if (name == null) return null;
|
||||
// Strip common suffixes
|
||||
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||
|
||||
@@ -23,11 +23,24 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
||||
boolean hasPolyMatch = false;
|
||||
for (String pe : polyEvents) {
|
||||
if (pe.contains(".") && smEventRaw.contains(".")) {
|
||||
if (smEventRaw.equals(pe) || smEventRaw.endsWith("." + pe)) {
|
||||
if (smEventRaw.equals(pe) || smEventRaw.endsWith("." + pe) || pe.endsWith("." + smEventRaw)) {
|
||||
hasPolyMatch = true;
|
||||
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;
|
||||
@@ -37,7 +50,8 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
||||
String simplifiedPe = simplify(simplePe);
|
||||
|
||||
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
||||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) {
|
||||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent) ||
|
||||
isGuardedContains(smEvent, simplifiedPe)) {
|
||||
hasPolyMatch = true;
|
||||
break;
|
||||
}
|
||||
@@ -47,20 +61,78 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
||||
if (polyEvents.isEmpty() && isWildcard) return true;
|
||||
|
||||
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 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);
|
||||
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) {
|
||||
return eventStr.equals("event") || eventStr.equals("e") ||
|
||||
eventStr.equals("msg") || eventStr.equals("message") ||
|
||||
eventStr.equals("payload");
|
||||
if (eventStr == null) return false;
|
||||
|
||||
if (eventStr.equals("event") || eventStr.equals("e") ||
|
||||
eventStr.equals("msg") || eventStr.equals("message") ||
|
||||
eventStr.equals("payload")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (eventStr.contains(" ? ") && eventStr.contains(" : ")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String simplify(String name) {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import java.util.List;
|
||||
|
||||
public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
||||
|
||||
@Override
|
||||
public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) {
|
||||
if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (triggerPoint.isExternal()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
||||
String rawTriggerEvent = triggerPoint.getEvent();
|
||||
|
||||
if (rawTriggerEvent != null && rawTriggerEvent.startsWith("<SYMBOLIC: ") && rawTriggerEvent.endsWith(".*>")) {
|
||||
String targetType = rawTriggerEvent.substring(11, rawTriggerEvent.length() - 3);
|
||||
if (smEventRaw.startsWith(targetType + ".")) {
|
||||
return true;
|
||||
}
|
||||
if (smEventRaw.contains(".")) {
|
||||
String smType = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
||||
if (smType.endsWith("." + targetType) || smType.equals(targetType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
||||
|
||||
for (String pe : polyEvents) {
|
||||
if (pe.equals(smEventRaw)) {
|
||||
return true;
|
||||
}
|
||||
if (pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
|
||||
String targetType = pe.substring(11, pe.length() - 3);
|
||||
if (smEventRaw.startsWith(targetType + ".")) {
|
||||
return true;
|
||||
}
|
||||
if (smEventRaw.contains(".")) {
|
||||
String smType = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
||||
if (smType.endsWith("." + targetType) || smType.equals(targetType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (polyEvents.isEmpty() && isWildcardVariable(rawTriggerEvent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return rawTriggerEvent.equals(smEventRaw);
|
||||
}
|
||||
|
||||
private boolean isWildcardVariable(String eventStr) {
|
||||
if (eventStr == null) return false;
|
||||
|
||||
if (eventStr.equals("event") || eventStr.equals("e") ||
|
||||
eventStr.equals("msg") || eventStr.equals("message") ||
|
||||
eventStr.equals("payload")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (eventStr.contains(" ? ") && eventStr.contains(" : ")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
|
||||
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;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -8,7 +9,35 @@ import java.util.Set;
|
||||
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
|
||||
@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();
|
||||
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||
@@ -40,6 +69,21 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
boolean hasPositiveMatch = 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()) {
|
||||
String chainClass = getClassNameOnly(method);
|
||||
String chainPackage = getPackageName(chainClass);
|
||||
@@ -54,7 +98,7 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
hasPositiveMatch = true;
|
||||
}
|
||||
|
||||
if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
|
||||
if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix, smSimple, chainSimple)) {
|
||||
hasStrongMismatch = true;
|
||||
}
|
||||
}
|
||||
@@ -102,37 +146,68 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
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 (smPrefix == null || chainPrefix == null) return false;
|
||||
|
||||
String[] p1 = smPackage.split("\\.");
|
||||
String[] p2 = chainPackage.split("\\.");
|
||||
String smLower = smPrefix.toLowerCase();
|
||||
String chainLower = chainPrefix.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;
|
||||
// Ignore generic/technical class prefixes
|
||||
if (smLower.equals("state") || smLower.equals("statemachine") || smLower.equals("config") || smLower.equals("configuration") || smLower.equals("adapter") ||
|
||||
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") ||
|
||||
chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config") || chainLower.equals("configuration") || chainLower.equals("adapter") ||
|
||||
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")) {
|
||||
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<>();
|
||||
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase());
|
||||
for (int i = matchIdx + 1; i < p1.length; i++) {
|
||||
smDivergentTerms.add(p1[i].toLowerCase());
|
||||
}
|
||||
// Otherwise, if they diverge under a shared project package root, it is a mismatch
|
||||
String[] p1 = smPackage.split("\\.");
|
||||
String[] p2 = chainPackage.split("\\.");
|
||||
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 (chainPrefix != null) chainDivergentTerms.add(chainPrefix.toLowerCase());
|
||||
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();
|
||||
if (!onlyGenericDivergence) {
|
||||
return true; // Mismatch under a shared root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -185,4 +260,120 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
}
|
||||
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,25 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CallEdge {
|
||||
private final String targetMethod;
|
||||
private final List<String> arguments;
|
||||
private String targetMethod;
|
||||
private List<String> arguments;
|
||||
private String receiver;
|
||||
private String constraint;
|
||||
|
||||
public CallEdge(String targetMethod, List<String> arguments) {
|
||||
this(targetMethod, arguments, null, null);
|
||||
}
|
||||
|
||||
public CallEdge(String targetMethod, List<String> arguments, String receiver) {
|
||||
this(targetMethod, arguments, receiver, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@Builder(toBuilder = true)
|
||||
@Jacksonized
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TriggerPoint {
|
||||
@@ -20,5 +20,106 @@ public class TriggerPoint {
|
||||
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 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 boolean external;
|
||||
private final String constraint;
|
||||
|
||||
public TriggerPoint(
|
||||
String event,
|
||||
String className,
|
||||
String methodName,
|
||||
String sourceFile,
|
||||
String sourceModule,
|
||||
String stateMachineId,
|
||||
String sourceState,
|
||||
int lineNumber,
|
||||
String stateTypeFqn,
|
||||
String eventTypeFqn,
|
||||
java.util.List<String> polymorphicEvents,
|
||||
boolean external,
|
||||
String constraint) {
|
||||
this.className = className;
|
||||
this.methodName = methodName;
|
||||
this.sourceFile = sourceFile;
|
||||
this.sourceModule = sourceModule;
|
||||
this.stateMachineId = stateMachineId;
|
||||
this.sourceState = sourceState;
|
||||
this.lineNumber = lineNumber;
|
||||
this.stateTypeFqn = stateTypeFqn;
|
||||
this.eventTypeFqn = eventTypeFqn;
|
||||
this.event = qualifyEvent(event, eventTypeFqn);
|
||||
if (polymorphicEvents != null) {
|
||||
this.polymorphicEvents = polymorphicEvents.stream()
|
||||
.map(pe -> qualifyEvent(pe, eventTypeFqn))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
} else {
|
||||
this.polymorphicEvents = null;
|
||||
}
|
||||
this.external = external;
|
||||
this.constraint = constraint;
|
||||
}
|
||||
|
||||
private static String qualifyEvent(String e, String eventTypeFqn) {
|
||||
if (e == null || eventTypeFqn == null || e.isEmpty()) {
|
||||
return e;
|
||||
}
|
||||
if (e.startsWith("<SYMBOLIC: ")) {
|
||||
return e;
|
||||
}
|
||||
if (isWildcardVariable(e)) {
|
||||
return e;
|
||||
}
|
||||
if (Character.isLowerCase(e.charAt(0))) {
|
||||
return e;
|
||||
}
|
||||
if (!isEnumConstantCandidate(e)) {
|
||||
return e;
|
||||
}
|
||||
if (e.contains(".")) {
|
||||
String qualifier = e.substring(0, e.lastIndexOf('.'));
|
||||
String lastSegment = e.substring(e.lastIndexOf('.') + 1);
|
||||
if (qualifier.equals(eventTypeFqn)) {
|
||||
return e;
|
||||
}
|
||||
if (eventTypeFqn.endsWith("." + qualifier) || eventTypeFqn.equals(qualifier)) {
|
||||
return eventTypeFqn + "." + lastSegment;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
if (eventTypeFqn.startsWith("java.lang.") || eventTypeFqn.equals("int") || eventTypeFqn.equals("long") || eventTypeFqn.equals("char")) {
|
||||
return e;
|
||||
}
|
||||
return eventTypeFqn + "." + e;
|
||||
}
|
||||
|
||||
private static boolean isEnumConstantCandidate(String eventStr) {
|
||||
if (eventStr == null) return false;
|
||||
if (eventStr.startsWith("<SYMBOLIC: ")) {
|
||||
return false;
|
||||
}
|
||||
if (eventStr.contains(" ") || eventStr.contains("(") || eventStr.contains(")") || eventStr.contains("?")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isWildcardVariable(String eventStr) {
|
||||
if (eventStr == null) return false;
|
||||
if (eventStr.equals("event") || eventStr.equals("e") ||
|
||||
eventStr.equals("msg") || eventStr.equals("message") ||
|
||||
eventStr.equals("payload")) {
|
||||
return true;
|
||||
}
|
||||
if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) {
|
||||
return true;
|
||||
}
|
||||
if (eventStr.contains(" ? ") && eventStr.contains(" : ")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ public class ConstantResolver {
|
||||
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) {
|
||||
if (expr == null) return null;
|
||||
|
||||
@@ -64,6 +72,9 @@ public class ConstantResolver {
|
||||
String val = resolveManual(qn, context, visited);
|
||||
return val != null ? val : qn.toString();
|
||||
}
|
||||
if (expr instanceof FieldAccess fa) {
|
||||
return resolveManual(fa.getName(), context, visited);
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
return resolveManual(sn, context, visited);
|
||||
}
|
||||
@@ -82,9 +93,22 @@ public class ConstantResolver {
|
||||
|
||||
// Fallback for unresolved AST nodes
|
||||
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) {
|
||||
return sn.getIdentifier();
|
||||
System.out.println("RETURNING sn name: " + sn.getIdentifier()); return sn.getIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
if ("valueOf".equals(mi.getName().getIdentifier())) {
|
||||
String enumFqn = null;
|
||||
if (mi.arguments().size() == 2) {
|
||||
enumFqn = resolveEnumFqn((Expression) mi.arguments().get(0), context);
|
||||
} else if (mi.arguments().size() == 1 && mi.getExpression() != null) {
|
||||
enumFqn = resolveEnumFqn(mi.getExpression(), context);
|
||||
}
|
||||
|
||||
if (enumFqn != null) {
|
||||
return "<SYMBOLIC: " + enumFqn + ".*>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,13 +122,43 @@ public class ConstantResolver {
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||
if (md != null && md.getReturnType2() != null) {
|
||||
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
|
||||
if (values != null && !values.isEmpty()) {
|
||||
return "ENUM_SET:" + String.join(",", values);
|
||||
if (md != null) {
|
||||
String evaluated = evaluateMethodOutput(mi, md, context, visited);
|
||||
if (evaluated != null) return evaluated;
|
||||
|
||||
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 +168,275 @@ public class ConstantResolver {
|
||||
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) {
|
||||
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
|
||||
|
||||
@@ -134,6 +457,32 @@ public class ConstantResolver {
|
||||
}
|
||||
|
||||
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);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
@@ -193,6 +542,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 {
|
||||
visited.remove(fieldId);
|
||||
}
|
||||
@@ -238,4 +609,85 @@ public class ConstantResolver {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private String resolveEnumFqn(Expression expr, CodebaseContext context) {
|
||||
if (expr == null) return null;
|
||||
if (expr instanceof TypeLiteral tl) {
|
||||
ITypeBinding binding = tl.getType().resolveBinding();
|
||||
if (binding != null) {
|
||||
return binding.getQualifiedName();
|
||||
}
|
||||
String typeName = tl.getType().toString();
|
||||
if (typeName.endsWith(".class")) {
|
||||
typeName = typeName.substring(0, typeName.length() - 6);
|
||||
}
|
||||
TypeDeclaration td = context.getTypeDeclaration(typeName);
|
||||
if (td != null) return context.getFqn(td);
|
||||
return typeName;
|
||||
}
|
||||
|
||||
ITypeBinding binding = expr.resolveTypeBinding();
|
||||
if (binding != null) {
|
||||
if (binding.isClass() && "java.lang.Class".equals(binding.getErasure().getQualifiedName())) {
|
||||
ITypeBinding[] typeArgs = binding.getTypeArguments();
|
||||
if (typeArgs.length > 0) {
|
||||
return typeArgs[0].getQualifiedName();
|
||||
}
|
||||
}
|
||||
return binding.getQualifiedName();
|
||||
}
|
||||
|
||||
String exprStr = expr.toString();
|
||||
if (exprStr.endsWith(".class")) {
|
||||
exprStr = exprStr.substring(0, exprStr.length() - 6);
|
||||
}
|
||||
TypeDeclaration td = context.getTypeDeclaration(exprStr);
|
||||
if (td != null) return context.getFqn(td);
|
||||
return exprStr;
|
||||
}
|
||||
}
|
||||
|
||||
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,407 @@
|
||||
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 (constantResolver != null) {
|
||||
String resolved = constantResolver.resolve(expr, context);
|
||||
if (resolved != null) {
|
||||
constants.add(resolved);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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) ||
|
||||
"sendEventCollect".equals(methodName) || "sendEventMono".equals(methodName)) {
|
||||
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);
|
||||
@@ -46,6 +56,90 @@ public class GenericEventDetector {
|
||||
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);
|
||||
|
||||
boolean external = false;
|
||||
if (receiver != null) {
|
||||
AssignmentDag dag = new AssignmentDag(method);
|
||||
external = dag.isExternal(receiver, new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
if (eventValue != null) {
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
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])
|
||||
.external(external)
|
||||
.constraint(constraint)
|
||||
.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) {
|
||||
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
|
||||
if (builtTriggers != null) {
|
||||
@@ -141,6 +235,16 @@ public class GenericEventDetector {
|
||||
if (type == null) return Collections.emptyList();
|
||||
|
||||
String sourceState = extractSourceState(node);
|
||||
String[] smTypes = resolveStateMachineTypeArguments(node);
|
||||
|
||||
boolean external = false;
|
||||
if (!node.arguments().isEmpty()) {
|
||||
Expression eventExpr = (Expression) node.arguments().get(0);
|
||||
AssignmentDag dag = new AssignmentDag(method);
|
||||
external = dag.isExternal(eventExpr, new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
|
||||
List<TriggerPoint> results = new ArrayList<>();
|
||||
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
||||
@@ -154,6 +258,10 @@ public class GenericEventDetector {
|
||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||
.stateTypeFqn(smTypes[0])
|
||||
.eventTypeFqn(smTypes[1])
|
||||
.external(external)
|
||||
.constraint(constraint)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -165,6 +273,10 @@ public class GenericEventDetector {
|
||||
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||
.stateTypeFqn(smTypes[0])
|
||||
.eventTypeFqn(smTypes[1])
|
||||
.external(external)
|
||||
.constraint(constraint)
|
||||
.build());
|
||||
}
|
||||
|
||||
@@ -265,8 +377,26 @@ public class GenericEventDetector {
|
||||
return getSimpleNameString(right);
|
||||
}
|
||||
} else if (expr instanceof MethodInvocation mi) {
|
||||
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
|
||||
return getSimpleNameString((Expression) mi.arguments().get(0));
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
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;
|
||||
@@ -392,4 +522,228 @@ public class GenericEventDetector {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private static class AssignmentDag {
|
||||
final java.util.Map<String, List<Expression>> assignments = new java.util.HashMap<>();
|
||||
final java.util.Set<SingleVariableDeclaration> parameters = new java.util.HashSet<>();
|
||||
|
||||
AssignmentDag(MethodDeclaration method) {
|
||||
if (method == null) return;
|
||||
for (Object paramObj : method.parameters()) {
|
||||
if (paramObj instanceof SingleVariableDeclaration svd) {
|
||||
parameters.add(svd);
|
||||
}
|
||||
}
|
||||
if (method.getBody() != null) {
|
||||
method.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment fragment) {
|
||||
String varName = fragment.getName().getIdentifier();
|
||||
if (fragment.getInitializer() != null) {
|
||||
assignments.computeIfAbsent(varName, k -> new ArrayList<>()).add(fragment.getInitializer());
|
||||
}
|
||||
return super.visit(fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(Assignment assignment) {
|
||||
if (assignment.getLeftHandSide() instanceof SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
assignments.computeIfAbsent(varName, k -> new ArrayList<>()).add(assignment.getRightHandSide());
|
||||
}
|
||||
return super.visit(assignment);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
boolean isExternal(Expression expr, java.util.Set<String> visited) {
|
||||
if (expr == null) return false;
|
||||
Expression peeled = expr;
|
||||
while (peeled instanceof ParenthesizedExpression pe) {
|
||||
peeled = pe.getExpression();
|
||||
}
|
||||
while (peeled instanceof CastExpression ce) {
|
||||
peeled = ce.getExpression();
|
||||
}
|
||||
|
||||
if (peeled instanceof SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
for (SingleVariableDeclaration svd : parameters) {
|
||||
if (svd.getName().getIdentifier().equals(varName)) {
|
||||
return isAnnotatedAsExternal(svd);
|
||||
}
|
||||
}
|
||||
if (assignments.containsKey(varName)) {
|
||||
if (!visited.add(varName)) {
|
||||
return false;
|
||||
}
|
||||
List<Expression> defs = assignments.get(varName);
|
||||
for (Expression def : defs) {
|
||||
if (isExternal(def, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (peeled instanceof MethodInvocation mi) {
|
||||
if (mi.getExpression() != null && isExternal(mi.getExpression(), visited)) {
|
||||
return true;
|
||||
}
|
||||
for (Object arg : mi.arguments()) {
|
||||
if (isExternal((Expression) arg, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAnnotatedAsExternal(SingleVariableDeclaration svd) {
|
||||
for (Object modifier : svd.modifiers()) {
|
||||
if (modifier instanceof Annotation annotation) {
|
||||
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||
if (typeName.endsWith("PathVariable") || typeName.endsWith("RequestBody") || typeName.endsWith("RequestParam")
|
||||
|| typeName.endsWith("PathParam") || typeName.endsWith("QueryParam")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
for (String calledMethod : calledMethods) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
for (Object argObj : node.arguments()) {
|
||||
if (argObj instanceof ExpressionMethodReference emr) {
|
||||
@@ -45,7 +49,16 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (td2 != null) {
|
||||
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||
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());
|
||||
CallEdge edge = new CallEdge(refMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -58,11 +71,22 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
if (fallbackTypeFqn != null) {
|
||||
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());
|
||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||
CallEdge implEdge = new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver);
|
||||
implEdge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(implEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +118,11 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
calledMethod = superFqn + "." + methodName;
|
||||
}
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
String receiver = "super";
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,62 +134,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return graph;
|
||||
}
|
||||
|
||||
protected List<String> resolveArguments(List<?> astArguments) {
|
||||
List<String> args = new ArrayList<>();
|
||||
for (Object argObj : astArguments) {
|
||||
Expression expr = (Expression) argObj;
|
||||
@Override
|
||||
protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
|
||||
// Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
|
||||
// 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 (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);
|
||||
if (isThisOrSuperGetter) {
|
||||
return originalExpr.toString(); // Preserve "this.getTransitionType()" or "super.getEvent()" for later substitution
|
||||
}
|
||||
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) {
|
||||
@@ -175,6 +172,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||
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);
|
||||
for (String impl : impls) {
|
||||
allResolved.add(impl + "." + methodName);
|
||||
@@ -184,14 +187,32 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
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) {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
|
||||
if (receiver instanceof MethodInvocation miReceiver) {
|
||||
String returnType = resolveMethodInvocationReturnType(miReceiver);
|
||||
if (returnType != null) {
|
||||
return returnType + "." + methodName;
|
||||
}
|
||||
}
|
||||
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return resolveMethodInType(td, methodName);
|
||||
return resolveMethodInTypeHierarchy(td, methodName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -217,12 +238,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
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;
|
||||
}
|
||||
|
||||
protected String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
||||
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
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||
if (enclosingMethod != null) {
|
||||
@@ -254,10 +294,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);
|
||||
if (enclosingType != null) {
|
||||
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||
TypeDeclaration currentType = enclosingType;
|
||||
while (currentType != null) {
|
||||
for (FieldDeclaration field : currentType.getFields()) {
|
||||
for (Object fragObj : field.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
@@ -265,6 +306,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
String superclass = context.getSuperclassFqn(currentType);
|
||||
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -299,13 +342,299 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
protected String resolveMethodInType(TypeDeclaration td, String methodName) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
TypeDeclaration declaringTd = findEnclosingType(md);
|
||||
return context.getFqn(declaringTd) + "." + methodName;
|
||||
/**
|
||||
* Attempts to narrow the resolution of {@code instanceVar.getterMethod()} when the
|
||||
* instance variable is initialised from a {@link org.eclipse.jdt.core.dom.ClassInstanceCreation}
|
||||
* (either directly or via a factory/mapper method).
|
||||
*
|
||||
* <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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,8 +38,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
for (String calledMethod : calledMethods) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
CallEdge edge = new CallEdge(calledMethod, args);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
for (Object argObj : node.arguments()) {
|
||||
if (argObj instanceof ExpressionMethodReference emr) {
|
||||
@@ -49,7 +52,15 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (td2 != null) {
|
||||
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||
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);
|
||||
}
|
||||
CallEdge edge = new CallEdge(refMethod, implicitArgs);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -62,11 +73,21 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
if (fallbackTypeFqn != null) {
|
||||
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);
|
||||
}
|
||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||
CallEdge implEdge = new CallEdge(impl + "." + emr.getName().getIdentifier(), args);
|
||||
implEdge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(implEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +119,10 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
calledMethod = superFqn + "." + methodName;
|
||||
}
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
CallEdge edge = new CallEdge(calledMethod, args);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,62 +134,28 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return graph;
|
||||
}
|
||||
|
||||
private List<String> resolveArguments(List<?> astArguments) {
|
||||
List<String> args = new ArrayList<>();
|
||||
for (Object argObj : astArguments) {
|
||||
Expression expr = (Expression) argObj;
|
||||
|
||||
// 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);
|
||||
@Override
|
||||
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
|
||||
if (expr == null) {
|
||||
return "null";
|
||||
}
|
||||
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) {
|
||||
@@ -192,6 +182,13 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
|
||||
if (receiver instanceof MethodInvocation miReceiver) {
|
||||
String returnType = resolveMethodInvocationReturnType(miReceiver);
|
||||
if (returnType != null) {
|
||||
return returnType + "." + methodName;
|
||||
}
|
||||
}
|
||||
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
@@ -303,10 +300,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);
|
||||
if (enclosingType != null) {
|
||||
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||
TypeDeclaration currentType = enclosingType;
|
||||
while (currentType != null) {
|
||||
for (FieldDeclaration field : currentType.getFields()) {
|
||||
for (Object fragObj : field.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
@@ -314,6 +312,8 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
String superclass = context.getSuperclassFqn(currentType);
|
||||
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -348,13 +348,65 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
private String resolveMethodInType(TypeDeclaration td, String methodName) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
TypeDeclaration declaringTd = findEnclosingType(md);
|
||||
return context.getFqn(declaringTd) + "." + methodName;
|
||||
private Expression unwrapMessageBuilder(Expression expr) {
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
MethodInvocation current = mi;
|
||||
while (current != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,651 @@
|
||||
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);
|
||||
}
|
||||
|
||||
private Expression peelExpression(Expression expr) {
|
||||
while (expr instanceof ParenthesizedExpression pe) {
|
||||
expr = pe.getExpression();
|
||||
}
|
||||
if (expr instanceof CastExpression ce) {
|
||||
return peelExpression(ce.getExpression());
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private String getFieldType(TypeDeclaration td, String fieldName, CodebaseContext context, java.util.Set<String> visited) {
|
||||
if (td == null) return null;
|
||||
String typeFqn = context.getFqn(td);
|
||||
if (!visited.add(typeFqn)) return null;
|
||||
|
||||
for (FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(fieldName)) {
|
||||
return fd.getType().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
if (superTd != null) {
|
||||
return getFieldType(superTd, fieldName, context, visited);
|
||||
}
|
||||
}
|
||||
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 including superclasses
|
||||
String fieldType = getFieldType(td, cleanFieldName, context, new java.util.HashSet<>());
|
||||
if (fieldType != null) return fieldType;
|
||||
}
|
||||
}
|
||||
|
||||
// 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(peelExpression(node.getInitializer()));
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(Assignment node) {
|
||||
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||
initializers.add(peelExpression(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 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 = peelExpression((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 = peelExpression((Expression) mi.arguments().get(0));
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
return mi;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -361,6 +361,115 @@ public class AstTransitionParser {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
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.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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class StateMachineAggregator {
|
||||
private final CodebaseContext context;
|
||||
|
||||
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");
|
||||
}
|
||||
private final StateMachineAdapterParser adapterParser;
|
||||
|
||||
@Getter
|
||||
private final Set<String> initialStates = new HashSet<>();
|
||||
@Getter
|
||||
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) {
|
||||
Set<MethodVisit> visitedMethods = new HashSet<>();
|
||||
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
|
||||
}
|
||||
|
||||
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();
|
||||
StateMachineAdapterParser.AdapterStates states = adapterParser.parseStates(startClass);
|
||||
this.initialStates.clear();
|
||||
this.initialStates.addAll(states.initialStates());
|
||||
this.endStates.clear();
|
||||
this.endStates.addAll(states.endStates());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,85 @@ public final class AstUtils {
|
||||
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;
|
||||
}
|
||||
|
||||
public static String findConditionConstraint(org.eclipse.jdt.core.dom.ASTNode node) {
|
||||
java.util.List<String> conditions = new java.util.ArrayList<>();
|
||||
org.eclipse.jdt.core.dom.ASTNode current = node;
|
||||
while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
|
||||
org.eclipse.jdt.core.dom.ASTNode parent = current.getParent();
|
||||
if (parent instanceof org.eclipse.jdt.core.dom.IfStatement ifStmt) {
|
||||
if (ifStmt.getExpression() != current) {
|
||||
org.eclipse.jdt.core.dom.Expression cond = ifStmt.getExpression();
|
||||
if (current == ifStmt.getElseStatement()) {
|
||||
conditions.add("!(" + cond.toString() + ")");
|
||||
} else {
|
||||
conditions.add(cond.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
if (conditions.isEmpty()) return null;
|
||||
java.util.Collections.reverse(conditions);
|
||||
return String.join(" && ", conditions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 Set<String> ambiguousSimpleNames = new HashSet<>();
|
||||
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||
private final StateResolver stateResolver = new StateResolver(this);
|
||||
private final PropertyResolver propertyResolver = new PropertyResolver();
|
||||
|
||||
public ConstantResolver getConstantResolver() {
|
||||
return constantResolver;
|
||||
}
|
||||
private final List<String> activeProfiles = new ArrayList<>();
|
||||
private Map<String, Map<String, String>> allProperties = new HashMap<>();
|
||||
private List<LibraryHint> libraryHints = new ArrayList<>();
|
||||
@@ -65,7 +70,7 @@ public class CodebaseContext {
|
||||
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[] sourcepath = new String[0];
|
||||
private boolean resolveBindings = false;
|
||||
@@ -135,7 +140,7 @@ public class CodebaseContext {
|
||||
if (imp.isOnDemand()) {
|
||||
// Star import: import static com.example.C.*;
|
||||
TypeDeclaration td = getTypeDeclaration(impName, contextCu);
|
||||
if (td != null && hasField(td, memberName)) {
|
||||
if (td != null && (hasField(td, memberName) || hasMethod(td, memberName))) {
|
||||
return td;
|
||||
}
|
||||
} else {
|
||||
@@ -150,6 +155,15 @@ public class CodebaseContext {
|
||||
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) {
|
||||
for (FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
@@ -191,6 +205,8 @@ public class CodebaseContext {
|
||||
indexType(td, packageName, javaFile);
|
||||
} else if (type instanceof EnumDeclaration ed) {
|
||||
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
|
||||
for (Object type : td.getTypes()) {
|
||||
if (type instanceof TypeDeclaration nestedTd) {
|
||||
for (Object decl : td.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
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) {
|
||||
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;
|
||||
String fqn = simpleNameToFqn.get(fqnOrSimpleName);
|
||||
String fqn = simpleNameToFqn.get(cleanName);
|
||||
if (fqn != null) return enumValues.get(fqn);
|
||||
// Also try stripping array or generic types if any (e.g. MyEnum[])
|
||||
return null;
|
||||
}
|
||||
|
||||
public State resolveState(Expression expr, CompilationUnit cu) {
|
||||
if (expr == null)
|
||||
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;
|
||||
return stateResolver.resolveState(expr, cu);
|
||||
}
|
||||
|
||||
private CompilationUnit parse(String source, String unitName) {
|
||||
@@ -412,6 +382,7 @@ public class CodebaseContext {
|
||||
parser.setSource(source.toCharArray());
|
||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||
parser.setResolveBindings(resolveBindings);
|
||||
parser.setBindingsRecovery(true);
|
||||
|
||||
Map<String, String> options = JavaCore.getOptions();
|
||||
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
|
||||
@@ -457,6 +428,14 @@ public class CodebaseContext {
|
||||
|
||||
// 2. Check imports in contextCu
|
||||
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()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
@@ -493,11 +472,20 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
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();
|
||||
if (superType == null) return null;
|
||||
|
||||
String superName = extractTypeName(superType);
|
||||
CompilationUnit cu = (CompilationUnit) td.getRoot();
|
||||
CompilationUnit cu = (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null;
|
||||
TypeDeclaration superTd = getTypeDeclaration(superName, cu);
|
||||
return superTd != null ? getFqn(superTd) : superName;
|
||||
}
|
||||
@@ -514,6 +502,56 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
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()) {
|
||||
if (method.getName().getIdentifier().equals(methodName)) {
|
||||
return method;
|
||||
@@ -525,7 +563,7 @@ public class CodebaseContext {
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
||||
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()) {
|
||||
Type interfaceType = (Type) interfaceTypeObj;
|
||||
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) {
|
||||
MethodDeclaration found = findMethodDeclaration(interfaceTd, methodName, true);
|
||||
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true);
|
||||
if (found != null) return found;
|
||||
}
|
||||
}
|
||||
@@ -543,15 +581,107 @@ public class CodebaseContext {
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getFqn(TypeDeclaration td) {
|
||||
StringBuilder sb = new StringBuilder(td.getName().getIdentifier());
|
||||
ASTNode current = td.getParent();
|
||||
while (current instanceof TypeDeclaration parent) {
|
||||
public AbstractTypeDeclaration getAbstractTypeDeclaration(String name) {
|
||||
// Try exact FQN match first
|
||||
CompilationUnit cu = classes.get(name);
|
||||
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() + ".");
|
||||
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 fqn = sb.toString();
|
||||
return packageName.isEmpty() ? fqn : packageName + "." + fqn;
|
||||
@@ -625,10 +755,19 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
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);
|
||||
if (visited.contains(fqn)) {
|
||||
return false;
|
||||
@@ -645,7 +784,7 @@ public class CodebaseContext {
|
||||
if (knownAdapters.contains(superclassName))
|
||||
return true;
|
||||
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
||||
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd, visited))
|
||||
if (superTd != null && extendsStateMachineConfigurerAdapterLegacy(superTd, visited))
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -656,7 +795,7 @@ public class CodebaseContext {
|
||||
if (knownAdapters.contains(interfaceName))
|
||||
return true;
|
||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
||||
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd, visited))
|
||||
if (interfaceTd != null && extendsStateMachineConfigurerAdapterLegacy(interfaceTd, visited))
|
||||
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 PropertyEnricher(),
|
||||
new CallChainEnricher(),
|
||||
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
|
||||
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -158,6 +158,24 @@ public class GoldenUpdater {
|
||||
Path.of("state_machines/inheritance_extra_functions3_state_machine"),
|
||||
Path.of("src/test/resources/golden/G2StateMachineConfiguration"),
|
||||
"G2StateMachineConfiguration"
|
||||
),
|
||||
new TestScenario(
|
||||
"Enterprise Order State Machine",
|
||||
Path.of("state_machines/state_machine_enterprise"),
|
||||
Path.of("src/test/resources/golden/OrderStateMachineConfiguration"),
|
||||
"OrderStateMachineConfiguration"
|
||||
),
|
||||
new TestScenario(
|
||||
"Enterprise Document State Machine",
|
||||
Path.of("state_machines/state_machine_enterprise"),
|
||||
Path.of("src/test/resources/golden/DocumentStateMachineConfiguration"),
|
||||
"DocumentStateMachineConfiguration"
|
||||
),
|
||||
new TestScenario(
|
||||
"Enterprise User State Machine",
|
||||
Path.of("state_machines/state_machine_enterprise"),
|
||||
Path.of("src/test/resources/golden/UserStateMachineConfiguration"),
|
||||
"UserStateMachineConfiguration"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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;
|
||||
|
||||
@@ -88,4 +89,42 @@ public class ResolutionRobustnessTest {
|
||||
assertThat(context.getTypeDeclaration("p1.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();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class StrictFqnMatchingEngineTest {
|
||||
|
||||
private final StrictFqnMatchingEngine engine = new StrictFqnMatchingEngine();
|
||||
|
||||
@Test
|
||||
void shouldMatchExactFqn() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("PAY")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchEnumQualifierToFqn() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("OrderEvents.PAY")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreventCrossEnumContamination() {
|
||||
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("OrderEvents.PAY")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreventCrossEnumContaminationWithPolymorphicEvents() {
|
||||
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("getType()")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.polymorphicEvents(List.of("OrderEvents.PAY"))
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchPolymorphicEnumQualifierToFqn() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("getType()")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.polymorphicEvents(List.of("OrderEvents.PAY"))
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchRawStringToFqn() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("PAY")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchRawStringToRawString() {
|
||||
Event smEvent = Event.of("PAY", "PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("PAY").build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchPolymorphicRawStringToFqn() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("getType()")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.polymorphicEvents(List.of("PAY"))
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWildcards() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("event").build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchMismatchedFqn() {
|
||||
Event smEvent = Event.of("CREATE", "com.example.other.OrderEvent.CREATE");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("CREATE")
|
||||
.eventTypeFqn("com.example.some.OrderEvent")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public class HeuristicBeanResolutionEngineTest {
|
||||
|
||||
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
|
||||
// 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";
|
||||
|
||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||
|
||||
// Same exact domain, should match
|
||||
assertTrue(result, "Same domain should be accepted");
|
||||
@@ -51,7 +51,7 @@ public class HeuristicBeanResolutionEngineTest {
|
||||
|
||||
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
|
||||
|
||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||
|
||||
// Subpackage should match
|
||||
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";
|
||||
|
||||
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
|
||||
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";
|
||||
|
||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||
|
||||
// Mismatched domains, but explicit variable targeting works
|
||||
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
|
||||
|
||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
|
||||
boolean result = engine.isRoutedToCorrectMachine(chain, machineName, null);
|
||||
|
||||
// Explicit variable targeting completely conflicts
|
||||
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);
|
||||
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" +
|
||||
"public class MyService {\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" +
|
||||
" stateMachine.sendEvent(this.getEvent());\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,473 @@
|
||||
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().getEvent()).isEqualTo("mapToEnum(str)");
|
||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("<SYMBOLIC: MyEvents.*>");
|
||||
}
|
||||
}
|
||||
@@ -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,165 @@ class LocalVariableTest {
|
||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||
.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");
|
||||
}
|
||||
@Test
|
||||
void shouldUnwrapExplicitPassthroughMethod(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
private EventValidator validator;
|
||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
||||
OrderEvents resolved = validator.passthrough(domainEvent.getType());
|
||||
service.updateOrderState(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
class EventValidator {
|
||||
public OrderEvents passthrough(OrderEvents e) {
|
||||
if (e == null) throw new IllegalArgumentException();
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
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.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallbackToPrefixMatchingForExternalMethods(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private OrderService service;
|
||||
public void processOrderEvent(RichOrderEvent domainEvent) {
|
||||
OrderEvents resolved = UnknownExternalClass.validate(domainEvent.getType());
|
||||
service.updateOrderState(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
.doesNotContain("OrderEvents.PAY");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
digraph statemachine {
|
||||
rankdir=LR;
|
||||
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
||||
edge [fontname="Arial", fontsize=10];
|
||||
|
||||
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
||||
_start -> DRAFT;
|
||||
APPROVED [fillcolor=lightgray];
|
||||
DRAFT -> IN_REVIEW [label="DocumentEvent.SUBMIT", style="solid", color="black"];
|
||||
IN_REVIEW -> APPROVED [label="DocumentEvent.APPROVE", style="solid", color="black"];
|
||||
IN_REVIEW -> DRAFT [label="DocumentEvent.REJECT", style="solid", color="black"];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,676 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.PAY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 30,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.SHIP",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 37,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 44,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 51,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 58,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 65,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 72,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "ORDER",
|
||||
"lineNumber" : 81,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "DOCUMENT",
|
||||
"lineNumber" : 87,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "USER",
|
||||
"lineNumber" : 93,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/pay",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/ship",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/submit",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/approve",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/approve",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/reject",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/reject",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/verify",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/verify",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/suspend",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/suspend",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/pay",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.payOrder", "click.kamil.enterprise.web.StateMachineDispatcher.payOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.PAY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 30,
|
||||
"polymorphicEvents" : [ "OrderEvent.PAY" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/ship",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.shipOrder", "click.kamil.enterprise.web.StateMachineDispatcher.shipOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.SHIP",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 37,
|
||||
"polymorphicEvents" : [ "OrderEvent.SHIP" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/submit",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.submitDocument", "click.kamil.enterprise.web.StateMachineDispatcher.submitDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 44,
|
||||
"polymorphicEvents" : [ "DocumentEvent.SUBMIT" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "DocumentState.DRAFT",
|
||||
"targetState" : "DocumentState.IN_REVIEW",
|
||||
"event" : "DocumentEvent.SUBMIT"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/approve",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/approve",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.approveDocument", "click.kamil.enterprise.web.StateMachineDispatcher.approveDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 51,
|
||||
"polymorphicEvents" : [ "DocumentEvent.APPROVE" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "DocumentState.IN_REVIEW",
|
||||
"targetState" : "DocumentState.APPROVED",
|
||||
"event" : "DocumentEvent.APPROVE"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/reject",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/reject",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.rejectDocument", "click.kamil.enterprise.web.StateMachineDispatcher.rejectDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 58,
|
||||
"polymorphicEvents" : [ "DocumentEvent.REJECT" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "DocumentState.IN_REVIEW",
|
||||
"targetState" : "DocumentState.DRAFT",
|
||||
"event" : "DocumentEvent.REJECT"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/verify",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/verify",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.verifyUser", "click.kamil.enterprise.web.StateMachineDispatcher.verifyUser" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 65,
|
||||
"polymorphicEvents" : [ "UserEvent.VERIFY" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/suspend",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/suspend",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.suspendUser", "click.kamil.enterprise.web.StateMachineDispatcher.suspendUser" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 72,
|
||||
"polymorphicEvents" : [ "UserEvent.SUSPEND" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 81,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "DocumentState.DRAFT",
|
||||
"targetState" : "DocumentState.IN_REVIEW",
|
||||
"event" : "DocumentEvent.SUBMIT"
|
||||
}, {
|
||||
"sourceState" : "DocumentState.IN_REVIEW",
|
||||
"targetState" : "DocumentState.APPROVED",
|
||||
"event" : "DocumentEvent.APPROVE"
|
||||
}, {
|
||||
"sourceState" : "DocumentState.IN_REVIEW",
|
||||
"targetState" : "DocumentState.DRAFT",
|
||||
"event" : "DocumentEvent.REJECT"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 87,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "DocumentState.DRAFT",
|
||||
"targetState" : "DocumentState.IN_REVIEW",
|
||||
"event" : "DocumentEvent.SUBMIT"
|
||||
}, {
|
||||
"sourceState" : "DocumentState.IN_REVIEW",
|
||||
"targetState" : "DocumentState.APPROVED",
|
||||
"event" : "DocumentEvent.APPROVE"
|
||||
}, {
|
||||
"sourceState" : "DocumentState.IN_REVIEW",
|
||||
"targetState" : "DocumentState.DRAFT",
|
||||
"event" : "DocumentEvent.REJECT"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 93,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "DocumentState.DRAFT",
|
||||
"targetState" : "DocumentState.IN_REVIEW",
|
||||
"event" : "DocumentEvent.SUBMIT"
|
||||
}, {
|
||||
"sourceState" : "DocumentState.IN_REVIEW",
|
||||
"targetState" : "DocumentState.APPROVED",
|
||||
"event" : "DocumentEvent.APPROVE"
|
||||
}, {
|
||||
"sourceState" : "DocumentState.IN_REVIEW",
|
||||
"targetState" : "DocumentState.DRAFT",
|
||||
"event" : "DocumentEvent.REJECT"
|
||||
} ]
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.enterprise.machines.document.DocumentStateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "DocumentState.DRAFT" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "DocumentState.DRAFT",
|
||||
"fullIdentifier" : "DocumentState.DRAFT"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "DocumentState.IN_REVIEW",
|
||||
"fullIdentifier" : "DocumentState.IN_REVIEW"
|
||||
} ],
|
||||
"event" : {
|
||||
"rawName" : "DocumentEvent.SUBMIT",
|
||||
"fullIdentifier" : "DocumentEvent.SUBMIT"
|
||||
},
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "DocumentState.IN_REVIEW",
|
||||
"fullIdentifier" : "DocumentState.IN_REVIEW"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "DocumentState.APPROVED",
|
||||
"fullIdentifier" : "DocumentState.APPROVED"
|
||||
} ],
|
||||
"event" : {
|
||||
"rawName" : "DocumentEvent.APPROVE",
|
||||
"fullIdentifier" : "DocumentEvent.APPROVE"
|
||||
},
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "DocumentState.IN_REVIEW",
|
||||
"fullIdentifier" : "DocumentState.IN_REVIEW"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "DocumentState.DRAFT",
|
||||
"fullIdentifier" : "DocumentState.DRAFT"
|
||||
} ],
|
||||
"event" : {
|
||||
"rawName" : "DocumentEvent.REJECT",
|
||||
"fullIdentifier" : "DocumentEvent.REJECT"
|
||||
},
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"endStates" : [ "DocumentState.APPROVED" ]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,33 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
set separator none
|
||||
hide empty description
|
||||
hide stereotype
|
||||
skinparam state {
|
||||
BackgroundColor white
|
||||
BorderColor #94a3b8
|
||||
BorderThickness 1
|
||||
FontName Inter
|
||||
FontSize 9
|
||||
FontStyle bold
|
||||
RoundCorner 20
|
||||
Padding 1
|
||||
}
|
||||
skinparam shadowing false
|
||||
skinparam ArrowFontName JetBrains Mono
|
||||
skinparam ArrowFontSize 8
|
||||
skinparam ArrowColor #cbd5e1
|
||||
skinparam ArrowThickness 1
|
||||
skinparam dpi 110
|
||||
skinparam svgLinkTarget _self
|
||||
|
||||
[*] --> DocumentState.DRAFT
|
||||
|
||||
|
||||
DocumentState.DRAFT -[#1E90FF,bold]-> DocumentState.IN_REVIEW <<external>> : DocumentEvent.SUBMIT
|
||||
DocumentState.IN_REVIEW -[#1E90FF,bold]-> DocumentState.APPROVED <<external>> : DocumentEvent.APPROVE
|
||||
DocumentState.IN_REVIEW -[#1E90FF,bold]-> DocumentState.DRAFT <<external>> : DocumentEvent.REJECT
|
||||
|
||||
DocumentState.APPROVED --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="DRAFT">
|
||||
<state id="DRAFT">
|
||||
<transition target="IN_REVIEW" event="DocumentEvent.SUBMIT"/>
|
||||
</state>
|
||||
<state id="IN_REVIEW">
|
||||
<transition target="APPROVED" event="DocumentEvent.APPROVE"/>
|
||||
<transition target="DRAFT" event="DocumentEvent.REJECT"/>
|
||||
</state>
|
||||
<state id="APPROVED">
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
}, {
|
||||
"event" : "PLACE_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
@@ -19,7 +21,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
}, {
|
||||
"event" : "CANCEL_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
@@ -29,7 +33,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
}, {
|
||||
"event" : "PAY_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
||||
@@ -39,7 +45,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
}, {
|
||||
"event" : "SHIP_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||
@@ -49,7 +57,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
}, {
|
||||
"event" : "RETURN_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||
@@ -59,7 +69,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -192,7 +204,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : [ "PLACE_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -227,7 +241,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : [ "CANCEL_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -257,7 +273,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
@@ -288,7 +306,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : [ "PAY_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -323,7 +343,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -358,7 +380,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 15,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -57,7 +59,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 15,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : [ "INHERITED_SUBMIT" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "SUBMIT",
|
||||
"event" : "String.SUBMIT",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
|
||||
@@ -9,9 +9,11 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 40,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
}, {
|
||||
"event" : "ORDER_EVENT",
|
||||
"event" : "String.ORDER_EVENT",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
|
||||
"methodName" : "onMessage",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
|
||||
@@ -19,7 +21,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 52,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -98,7 +102,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.maven.core.MavenOrderStateMachine.OrderController.submitOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "SUBMIT",
|
||||
"event" : "String.SUBMIT",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
|
||||
@@ -106,14 +110,12 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 40,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "INIT",
|
||||
"targetState" : "BUSY",
|
||||
"event" : "SUBMIT"
|
||||
} ]
|
||||
"matchedTransitions" : null
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "SUBMIT",
|
||||
"event" : "String.SUBMIT",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/multi/core/OrderController.java",
|
||||
@@ -9,7 +9,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -61,7 +63,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "SUBMIT",
|
||||
"event" : "String.SUBMIT",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/multi/core/OrderController.java",
|
||||
@@ -69,14 +71,12 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "NEW",
|
||||
"targetState" : "PROCESSING",
|
||||
"event" : "SUBMIT"
|
||||
} ]
|
||||
"matchedTransitions" : null
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph statemachine {
|
||||
rankdir=LR;
|
||||
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
||||
edge [fontname="Arial", fontsize=10];
|
||||
|
||||
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
||||
_start -> NEW;
|
||||
SHIPPED [fillcolor=lightgray];
|
||||
NEW -> PENDING [label="OrderEvent.PAY / λ", style="solid", color="black"];
|
||||
PENDING -> SHIPPED [label="OrderEvent.SHIP", style="solid", color="black"];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,650 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.PAY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 30,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.SHIP",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 37,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 44,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 51,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 58,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 65,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 72,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "ORDER",
|
||||
"lineNumber" : 81,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "DOCUMENT",
|
||||
"lineNumber" : 87,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "USER",
|
||||
"lineNumber" : 93,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/pay",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/ship",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/submit",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/approve",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/approve",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/reject",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/reject",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/verify",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/verify",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/suspend",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/suspend",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/pay",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.payOrder", "click.kamil.enterprise.web.StateMachineDispatcher.payOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.PAY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 30,
|
||||
"polymorphicEvents" : [ "OrderEvent.PAY" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderState.NEW",
|
||||
"targetState" : "OrderState.PENDING",
|
||||
"event" : "OrderEvent.PAY"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/ship",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.shipOrder", "click.kamil.enterprise.web.StateMachineDispatcher.shipOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.SHIP",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 37,
|
||||
"polymorphicEvents" : [ "OrderEvent.SHIP" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderState.PENDING",
|
||||
"targetState" : "OrderState.SHIPPED",
|
||||
"event" : "OrderEvent.SHIP"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/submit",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.submitDocument", "click.kamil.enterprise.web.StateMachineDispatcher.submitDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 44,
|
||||
"polymorphicEvents" : [ "DocumentEvent.SUBMIT" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/approve",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/approve",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.approveDocument", "click.kamil.enterprise.web.StateMachineDispatcher.approveDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 51,
|
||||
"polymorphicEvents" : [ "DocumentEvent.APPROVE" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/reject",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/reject",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.rejectDocument", "click.kamil.enterprise.web.StateMachineDispatcher.rejectDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 58,
|
||||
"polymorphicEvents" : [ "DocumentEvent.REJECT" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/verify",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/verify",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.verifyUser", "click.kamil.enterprise.web.StateMachineDispatcher.verifyUser" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 65,
|
||||
"polymorphicEvents" : [ "UserEvent.VERIFY" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/suspend",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/suspend",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.suspendUser", "click.kamil.enterprise.web.StateMachineDispatcher.suspendUser" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 72,
|
||||
"polymorphicEvents" : [ "UserEvent.SUSPEND" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 81,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderState.NEW",
|
||||
"targetState" : "OrderState.PENDING",
|
||||
"event" : "OrderEvent.PAY"
|
||||
}, {
|
||||
"sourceState" : "OrderState.PENDING",
|
||||
"targetState" : "OrderState.SHIPPED",
|
||||
"event" : "OrderEvent.SHIP"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 87,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderState.NEW",
|
||||
"targetState" : "OrderState.PENDING",
|
||||
"event" : "OrderEvent.PAY"
|
||||
}, {
|
||||
"sourceState" : "OrderState.PENDING",
|
||||
"targetState" : "OrderState.SHIPPED",
|
||||
"event" : "OrderEvent.SHIP"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 93,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderState.NEW",
|
||||
"targetState" : "OrderState.PENDING",
|
||||
"event" : "OrderEvent.PAY"
|
||||
}, {
|
||||
"sourceState" : "OrderState.PENDING",
|
||||
"targetState" : "OrderState.SHIPPED",
|
||||
"event" : "OrderEvent.SHIP"
|
||||
} ]
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.enterprise.machines.order.OrderStateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "OrderState.NEW" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "OrderState.NEW",
|
||||
"fullIdentifier" : "OrderState.NEW"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "OrderState.PENDING",
|
||||
"fullIdentifier" : "OrderState.PENDING"
|
||||
} ],
|
||||
"event" : {
|
||||
"rawName" : "OrderEvent.PAY",
|
||||
"fullIdentifier" : "OrderEvent.PAY"
|
||||
},
|
||||
"guard" : null,
|
||||
"actions" : [ {
|
||||
"expression" : "context -> System.out.println(\"Payment processed.\")",
|
||||
"isLambda" : true,
|
||||
"internalLogic" : "context -> System.out.println(\"Payment processed.\")",
|
||||
"lineNumber" : 28,
|
||||
"className" : "click.kamil.enterprise.machines.order.OrderStateMachineConfiguration",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/machines/order/OrderStateMachineConfiguration.java"
|
||||
} ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "OrderState.PENDING",
|
||||
"fullIdentifier" : "OrderState.PENDING"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "OrderState.SHIPPED",
|
||||
"fullIdentifier" : "OrderState.SHIPPED"
|
||||
} ],
|
||||
"event" : {
|
||||
"rawName" : "OrderEvent.SHIP",
|
||||
"fullIdentifier" : "OrderEvent.SHIP"
|
||||
},
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"endStates" : [ "OrderState.SHIPPED" ]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,32 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
set separator none
|
||||
hide empty description
|
||||
hide stereotype
|
||||
skinparam state {
|
||||
BackgroundColor white
|
||||
BorderColor #94a3b8
|
||||
BorderThickness 1
|
||||
FontName Inter
|
||||
FontSize 9
|
||||
FontStyle bold
|
||||
RoundCorner 20
|
||||
Padding 1
|
||||
}
|
||||
skinparam shadowing false
|
||||
skinparam ArrowFontName JetBrains Mono
|
||||
skinparam ArrowFontSize 8
|
||||
skinparam ArrowColor #cbd5e1
|
||||
skinparam ArrowThickness 1
|
||||
skinparam dpi 110
|
||||
skinparam svgLinkTarget _self
|
||||
|
||||
[*] --> OrderState.NEW
|
||||
|
||||
|
||||
OrderState.NEW -[#1E90FF,bold]-> OrderState.PENDING <<external>> : OrderEvent.PAY / λ
|
||||
OrderState.PENDING -[#1E90FF,bold]-> OrderState.SHIPPED <<external>> : OrderEvent.SHIP
|
||||
|
||||
OrderState.SHIPPED --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="NEW">
|
||||
<state id="NEW">
|
||||
<transition target="PENDING" event="OrderEvent.PAY"/>
|
||||
</state>
|
||||
<state id="PENDING">
|
||||
<transition target="SHIPPED" event="OrderEvent.SHIP"/>
|
||||
</state>
|
||||
<state id="SHIPPED">
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
}, {
|
||||
"event" : "payload",
|
||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||
@@ -19,7 +21,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||
@@ -29,7 +33,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -191,7 +197,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -222,7 +230,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ "OrderEvents.FULFILL" ]
|
||||
"polymorphicEvents" : [ "OrderEvents.FULFILL" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -253,7 +263,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ "OrderEvents.CANCEL" ]
|
||||
"polymorphicEvents" : [ "OrderEvents.CANCEL" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -284,7 +296,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -315,7 +329,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -338,7 +354,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payCast", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "(BaseEvent)new PayEvent()",
|
||||
"event" : "new PayEvent()",
|
||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||
"methodName" : "processEvent",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||
@@ -346,7 +362,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ]
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -381,10 +399,20 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY", "OrderEvents.CANCEL" ],
|
||||
"external" : false,
|
||||
"constraint" : 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" : {
|
||||
"type" : "REST",
|
||||
@@ -400,7 +428,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "event",
|
||||
"event" : "new PayEvent()",
|
||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||
"methodName" : "processEvent",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||
@@ -408,25 +436,15 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : [ "OrderEvents.PAY" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderStates.SUBMITTED",
|
||||
"targetState" : "OrderStates.PAID",
|
||||
"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" : {
|
||||
@@ -443,7 +461,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderStatic", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "EventBuilder.buildEvent()",
|
||||
"event" : "new FulfillEvent()",
|
||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||
"methodName" : "processEvent",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||
@@ -451,10 +469,16 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvents.FULFILL" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderStates.PAID",
|
||||
"targetState" : "OrderStates.FULFILLED",
|
||||
"event" : "OrderEvents.FULFILL"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
@@ -470,7 +494,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "new EventBuilder().buildInstanceEvent()",
|
||||
"event" : "new CancelEvent()",
|
||||
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
|
||||
"methodName" : "processEvent",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
|
||||
@@ -478,10 +502,16 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 13,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvents.CANCEL" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "OrderStates.SUBMITTED",
|
||||
"targetState" : "OrderStates.CANCELED",
|
||||
"event" : "OrderEvents.CANCEL"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
@@ -505,7 +535,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : [ "OrderEvents.ABCD" ]
|
||||
"polymorphicEvents" : [ "OrderEvents.ABCD" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -536,7 +568,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : [ "OrderEvents.ABCD", "OrderEvents.PAY" ]
|
||||
"polymorphicEvents" : [ "OrderEvents.ABCD", "OrderEvents.PAY" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : "event instanceof OrderEvent"
|
||||
}, {
|
||||
"event" : "eventProvider",
|
||||
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||
@@ -19,7 +21,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 50,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : "event != null"
|
||||
}, {
|
||||
"event" : "customMessage",
|
||||
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||
@@ -29,7 +33,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 78,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
@@ -144,7 +150,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.PROCESS" ],
|
||||
"external" : false,
|
||||
"constraint" : "event instanceof OrderEvent"
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -175,7 +183,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ],
|
||||
"external" : false,
|
||||
"constraint" : "event instanceof OrderEvent"
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -206,7 +216,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.CANCEL" ],
|
||||
"external" : false,
|
||||
"constraint" : "event instanceof OrderEvent"
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -241,7 +253,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ],
|
||||
"external" : false,
|
||||
"constraint" : "event instanceof OrderEvent"
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -276,7 +290,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 78,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.PROCESS" ],
|
||||
"external" : false,
|
||||
"constraint" : null
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -311,7 +327,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 25,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.PROCESS" ],
|
||||
"external" : false,
|
||||
"constraint" : "event instanceof OrderEvent"
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
@@ -346,7 +364,9 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 50,
|
||||
"polymorphicEvents" : [ ]
|
||||
"polymorphicEvents" : [ "OrderEvent.CANCEL" ],
|
||||
"external" : false,
|
||||
"constraint" : "event != null"
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph statemachine {
|
||||
rankdir=LR;
|
||||
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
||||
edge [fontname="Arial", fontsize=10];
|
||||
|
||||
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
||||
_start -> GUEST;
|
||||
VERIFIED [fillcolor=lightgray];
|
||||
GUEST -> REGISTERED [label="UserEvent.REGISTER", style="solid", color="black"];
|
||||
REGISTERED -> VERIFIED [label="UserEvent.VERIFY", style="solid", color="black"];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.PAY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 30,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.SHIP",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 37,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 44,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 51,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 58,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 65,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 72,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "ORDER",
|
||||
"lineNumber" : 81,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "DOCUMENT",
|
||||
"lineNumber" : 87,
|
||||
"polymorphicEvents" : null
|
||||
}, {
|
||||
"event" : "event",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "USER",
|
||||
"lineNumber" : 93,
|
||||
"polymorphicEvents" : null
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/pay",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/ship",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/submit",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/approve",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/approve",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/reject",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/reject",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/verify",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/verify",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/suspend",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/suspend",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/pay",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.payOrder", "click.kamil.enterprise.web.StateMachineDispatcher.payOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.PAY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 30,
|
||||
"polymorphicEvents" : [ "OrderEvent.PAY" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/ship",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.shipOrder", "click.kamil.enterprise.web.StateMachineDispatcher.shipOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.SHIP",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 37,
|
||||
"polymorphicEvents" : [ "OrderEvent.SHIP" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/submit",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.submitDocument", "click.kamil.enterprise.web.StateMachineDispatcher.submitDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 44,
|
||||
"polymorphicEvents" : [ "DocumentEvent.SUBMIT" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/approve",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/approve",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.approveDocument", "click.kamil.enterprise.web.StateMachineDispatcher.approveDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 51,
|
||||
"polymorphicEvents" : [ "DocumentEvent.APPROVE" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/reject",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/reject",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.rejectDocument", "click.kamil.enterprise.web.StateMachineDispatcher.rejectDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 58,
|
||||
"polymorphicEvents" : [ "DocumentEvent.REJECT" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/verify",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/verify",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.verifyUser", "click.kamil.enterprise.web.StateMachineDispatcher.verifyUser" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 65,
|
||||
"polymorphicEvents" : [ "UserEvent.VERIFY" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "UserState.REGISTERED",
|
||||
"targetState" : "UserState.VERIFIED",
|
||||
"event" : "UserEvent.VERIFY"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/suspend",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/suspend",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.suspendUser", "click.kamil.enterprise.web.StateMachineDispatcher.suspendUser" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 72,
|
||||
"polymorphicEvents" : [ "UserEvent.SUSPEND" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 81,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "UserState.GUEST",
|
||||
"targetState" : "UserState.REGISTERED",
|
||||
"event" : "UserEvent.REGISTER"
|
||||
}, {
|
||||
"sourceState" : "UserState.REGISTERED",
|
||||
"targetState" : "UserState.VERIFIED",
|
||||
"event" : "UserEvent.VERIFY"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 87,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "UserState.GUEST",
|
||||
"targetState" : "UserState.REGISTERED",
|
||||
"event" : "UserEvent.REGISTER"
|
||||
}, {
|
||||
"sourceState" : "UserState.REGISTERED",
|
||||
"targetState" : "UserState.VERIFIED",
|
||||
"event" : "UserEvent.VERIFY"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "transition",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/{machineType}/transition/{event}",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "machineType",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "event",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
}, {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.transition", "click.kamil.enterprise.web.StateMachineDispatcher.dispatch" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "true ? OrderEvent.valueOf(eventString.toUpperCase()) : true ? DocumentEvent.valueOf(eventString.toUpperCase()) : UserEvent.valueOf(eventString.toUpperCase())",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "dispatch",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 93,
|
||||
"polymorphicEvents" : [ ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "UserState.GUEST",
|
||||
"targetState" : "UserState.REGISTERED",
|
||||
"event" : "UserEvent.REGISTER"
|
||||
}, {
|
||||
"sourceState" : "UserState.REGISTERED",
|
||||
"targetState" : "UserState.VERIFIED",
|
||||
"event" : "UserEvent.VERIFY"
|
||||
} ]
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.enterprise.machines.user.UserStateMachineConfiguration",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "UserState.GUEST" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "UserState.GUEST",
|
||||
"fullIdentifier" : "UserState.GUEST"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "UserState.REGISTERED",
|
||||
"fullIdentifier" : "UserState.REGISTERED"
|
||||
} ],
|
||||
"event" : {
|
||||
"rawName" : "UserEvent.REGISTER",
|
||||
"fullIdentifier" : "UserEvent.REGISTER"
|
||||
},
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "UserState.REGISTERED",
|
||||
"fullIdentifier" : "UserState.REGISTERED"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "UserState.VERIFIED",
|
||||
"fullIdentifier" : "UserState.VERIFIED"
|
||||
} ],
|
||||
"event" : {
|
||||
"rawName" : "UserEvent.VERIFY",
|
||||
"fullIdentifier" : "UserEvent.VERIFY"
|
||||
},
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"endStates" : [ "UserState.VERIFIED" ]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,32 @@
|
||||
@startuml
|
||||
!pragma layout smetana
|
||||
set separator none
|
||||
hide empty description
|
||||
hide stereotype
|
||||
skinparam state {
|
||||
BackgroundColor white
|
||||
BorderColor #94a3b8
|
||||
BorderThickness 1
|
||||
FontName Inter
|
||||
FontSize 9
|
||||
FontStyle bold
|
||||
RoundCorner 20
|
||||
Padding 1
|
||||
}
|
||||
skinparam shadowing false
|
||||
skinparam ArrowFontName JetBrains Mono
|
||||
skinparam ArrowFontSize 8
|
||||
skinparam ArrowColor #cbd5e1
|
||||
skinparam ArrowThickness 1
|
||||
skinparam dpi 110
|
||||
skinparam svgLinkTarget _self
|
||||
|
||||
[*] --> UserState.GUEST
|
||||
|
||||
|
||||
UserState.GUEST -[#1E90FF,bold]-> UserState.REGISTERED <<external>> : UserEvent.REGISTER
|
||||
UserState.REGISTERED -[#1E90FF,bold]-> UserState.VERIFIED <<external>> : UserEvent.VERIFY
|
||||
|
||||
UserState.VERIFIED --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="GUEST">
|
||||
<state id="GUEST">
|
||||
<transition target="REGISTERED" event="UserEvent.REGISTER"/>
|
||||
</state>
|
||||
<state id="REGISTERED">
|
||||
<transition target="VERIFIED" event="UserEvent.VERIFY"/>
|
||||
</state>
|
||||
<state id="VERIFIED">
|
||||
</state>
|
||||
</scxml>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[
|
||||
{
|
||||
"methodFqn": "ExternalTaxService.calculateTax",
|
||||
"event": "FINALIZE"
|
||||
}
|
||||
]
|
||||
@@ -1,6 +0,0 @@
|
||||
[
|
||||
{
|
||||
"methodFqn": "ExternalNotificationService.sendExternalNotification",
|
||||
"event": "EXTERNAL_TRIGGER"
|
||||
}
|
||||
]
|
||||
30
state_machines/state_machine_enterprise/build.gradle
Normal file
30
state_machines/state_machine_enterprise/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.5.3'
|
||||
id 'io.spring.dependency-management' version '1.1.7'
|
||||
}
|
||||
|
||||
group = 'click.kamil'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
}
|
||||
|
||||
bootJar { enabled = false }
|
||||
jar { enabled = true }
|
||||
@@ -0,0 +1,11 @@
|
||||
package click.kamil.enterprise;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class EnterpriseStateMachineApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(EnterpriseStateMachineApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package click.kamil.enterprise.core;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public abstract class AbstractStateMachine<S extends Enum<S> & EnterpriseState, E extends Enum<E> & EnterpriseEvent> {
|
||||
|
||||
private S currentState;
|
||||
private final Map<E, S> transitions = new ConcurrentHashMap<>();
|
||||
|
||||
protected AbstractStateMachine(S initialState) {
|
||||
this.currentState = initialState;
|
||||
configureTransitions();
|
||||
}
|
||||
|
||||
public abstract String getMachineType();
|
||||
|
||||
protected abstract void configureTransitions();
|
||||
|
||||
protected void addTransition(E event, S targetState) {
|
||||
transitions.put(event, targetState);
|
||||
}
|
||||
|
||||
public synchronized void trigger(E event, TransitionContext<?> context) {
|
||||
S target = transitions.get(event);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Invalid event " + event + " for state " + currentState);
|
||||
}
|
||||
|
||||
executePreChecks(currentState, target, context);
|
||||
S oldState = currentState;
|
||||
currentState = target;
|
||||
onTransition(oldState, currentState, event, context);
|
||||
}
|
||||
|
||||
protected void executePreChecks(S from, S to, TransitionContext<?> context) {
|
||||
// Parent checking logic
|
||||
System.out.println("[Abstract] Executing pre-checks from " + from.name() + " to " + to.name() + " by user " + context.getUserId());
|
||||
}
|
||||
|
||||
protected abstract void onTransition(S from, S to, E event, TransitionContext<?> context);
|
||||
|
||||
public S getCurrentState() {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
public Class<E> getEventType() {
|
||||
return (Class<E>) transitions.keySet().iterator().next().getClass();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package click.kamil.enterprise.core;
|
||||
|
||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
|
||||
public abstract class AbstractStateMachineConfiguration<S extends Enum<S> & EnterpriseState, E extends Enum<E> & EnterpriseEvent> extends EnumStateMachineConfigurerAdapter<S, E> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<S, E> states) throws Exception {
|
||||
configureStates(states);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<S, E> transitions) throws Exception {
|
||||
configureTransitions(transitions);
|
||||
}
|
||||
|
||||
protected abstract void configureStates(StateMachineStateConfigurer<S, E> states) throws Exception;
|
||||
protected abstract void configureTransitions(StateMachineTransitionConfigurer<S, E> transitions) throws Exception;
|
||||
|
||||
protected void commonProtectedMethod() {
|
||||
System.out.println("Common protected method called in AbstractStateMachineConfiguration.");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user