Compare commits
100 Commits
master
...
96fc975170
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| c903b079d3 | |||
| 23d79d1930 | |||
| 2720296d14 | |||
| b8b180ab3d | |||
| fc267c43c6 | |||
| 968601eefc | |||
| e00f4dca81 | |||
| bf82cc3562 | |||
| 24b67be64b | |||
| 9f651e7dc9 | |||
| e06429cb02 | |||
| 9cf2d063b3 | |||
| 073df21395 | |||
| f9e6247eb3 | |||
| 76ac143370 | |||
| 9ca8b2fe37 | |||
| 56cdf96f2d | |||
| c37aa92c78 | |||
| 3ac057618f | |||
| d59f4ac166 | |||
| 19547d3c5f | |||
| ab37eb5d40 | |||
| bf9208d529 | |||
| 7077214c81 | |||
| 344e295106 | |||
| d6b1571f18 | |||
| e617fb1754 | |||
| 06dc0ba641 | |||
| ba412b905e | |||
| 198c5d830e | |||
| a383de5a5f | |||
| ef9ebe3512 | |||
| b480dc83ef | |||
| 0a23daae05 | |||
| e894566112 | |||
| 8d4ba0697e | |||
| d93d36e8ad | |||
| 5894303510 | |||
| 0c9e8de310 | |||
| 566a814671 | |||
| f48cdf080a | |||
| 1f4a1667c3 | |||
| 79ddb307c1 | |||
| a9d40aaf8c | |||
| ac05513b6c | |||
| bd44049e5e | |||
| 71bcfe65d7 | |||
| 38d708f85b | |||
| 2b6b727de6 | |||
| 744439c99d | |||
| ab474c0766 | |||
| 2251a587d9 | |||
| e2798b26cf | |||
| f85a68d9fc | |||
| 8641c7266f |
22
InspectGolden.java
Normal file
22
InspectGolden.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
import click.kamil.springstatemachineexporter.service.ExportService;
|
||||||
|
import click.kamil.springstatemachineexporter.exporter.*;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||||
|
|
||||||
|
public class InspectGolden {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
Path inputDir = Path.of("state_machines/complex_state_machine");
|
||||||
|
Path tempDir = Files.createTempDirectory("golden-update-");
|
||||||
|
|
||||||
|
List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
||||||
|
EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
|
||||||
|
ExportService exportService = new ExportService(exporters, enrichmentService);
|
||||||
|
|
||||||
|
exportService.runExporter(inputDir, tempDir, null, true);
|
||||||
|
|
||||||
|
try (var stream = Files.list(tempDir)) {
|
||||||
|
stream.forEach(System.out::println);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
README.md
62
README.md
@@ -1,34 +1,38 @@
|
|||||||
|
# Spring State Machine Explorer
|
||||||
|
|
||||||
```shell
|
Static analysis tool to extract and visualize Spring State Machines with deep code traceability.
|
||||||
brew install graphviz
|
|
||||||
|
## Core Modules
|
||||||
|
- **`state_machine_exporter`**: Java AST analyzer. Scans source code, resolves local Maven/Gradle dependencies, and extracts logic (Lambdas, Actions, Guards) into JSON/PUML.
|
||||||
|
- **`state_machine_exporter_html`**: Generates an interactive HTML portal with SVG diagrams and rich tooltips.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Extract Metadata (JSON)
|
||||||
|
```bash
|
||||||
|
./gradlew :state_machine_exporter:run --args="-i ./my-project -o ./out -f json"
|
||||||
```
|
```
|
||||||
|
|
||||||
```shell
|
### 2. Generate Interactive Portal (HTML)
|
||||||
dot -Tpng statemachine.dot -o statemachine.png
|
```bash
|
||||||
|
./gradlew :state_machine_exporter_html:run --args="-i ./my-project -o ./out_html"
|
||||||
|
```
|
||||||
|
*Supports `--profiles prod` to resolve Spring property placeholders.*
|
||||||
|
|
||||||
|
## Business Flows
|
||||||
|
Define sequence of events in `src/main/resources/flows.json`:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Order Success",
|
||||||
|
"description": "Happy path for order placement",
|
||||||
|
"steps": ["PAY", "CHECK_AVAILABILITY->PENDING", "SHIP"]
|
||||||
|
}
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## JSON Structure
|
||||||
```shell
|
- `metadata.entryPoints`: REST, WebFlux, and JMS entry points.
|
||||||
brew install plantuml
|
- `metadata.callChains`: Trace from API call to machine trigger (`sendEvent`).
|
||||||
```
|
- `transitions.actions[].internalLogic`: Extracted source code/lambdas.
|
||||||
```shell
|
- `metadata.properties`: Dictionary of all detected Spring profiles.
|
||||||
plantuml statemachine.puml
|
|
||||||
plantuml -tsvg statemachine.puml
|
|
||||||
```
|
|
||||||
Possible formats
|
|
||||||
```shell
|
|
||||||
-teps To generate images using EPS format
|
|
||||||
-thtml To generate HTML file for class diagram
|
|
||||||
-tlatex:nopreamble To generate images using LaTeX/Tikz format without preamble
|
|
||||||
-tlatex To generate images using LaTeX/Tikz format
|
|
||||||
-tpdf To generate images using PDF format
|
|
||||||
-tpng To generate images using PNG format (default)
|
|
||||||
-tscxml To generate SCXML file for state diagram
|
|
||||||
-tsvg To generate images using SVG format
|
|
||||||
-ttxt To generate images with ASCII art
|
|
||||||
-tutxt To generate images with ASCII art using Unicode characters
|
|
||||||
-tvdx To generate images using VDX format
|
|
||||||
-txmi To generate XMI file for class diagram
|
|
||||||
```
|
|
||||||
|
|
||||||
plantuml statemachine.scxml
|
|
||||||
|
|||||||
1
TODO.md
1
TODO.md
@@ -1,6 +1,5 @@
|
|||||||
1.extract from all possible formats (ask chatgpt)
|
1.extract from all possible formats (ask chatgpt)
|
||||||
|
|
||||||
[DONE] json
|
|
||||||
[TODO] xml, maybe yaml
|
[TODO] xml, maybe yaml
|
||||||
|
|
||||||
2. [PLAN] Support external dependencies (JARs/compiled classes):
|
2. [PLAN] Support external dependencies (JARs/compiled classes):
|
||||||
|
|||||||
3
lombok.config
Normal file
3
lombok.config
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
lombok.anyConstructor.addConstructorProperties = true
|
||||||
|
lombok.jacksonized.flagUsage = ALLOW
|
||||||
|
config.stopBubbling = true
|
||||||
26
plan-extended-anaylis/PLAN.md
Normal file
26
plan-extended-anaylis/PLAN.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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)`).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
42
plan-extended-anaylis/advanced_context.md
Normal file
42
plan-extended-anaylis/advanced_context.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Advanced Context Extraction: Payloads and Interceptors
|
||||||
|
|
||||||
|
## 1. Incoming Payload Analysis
|
||||||
|
To understand *what* data drives the state machine, we need to capture metadata about the incoming parameters at entry points.
|
||||||
|
|
||||||
|
### REST Endpoints
|
||||||
|
For methods annotated with `@PostMapping`, `@PutMapping`, etc.:
|
||||||
|
1. **Extract Parameters**: Scan method parameters for `@RequestBody`, `@PathVariable`, or `@RequestParam`.
|
||||||
|
2. **Type Extraction**: Capture the FQN of the parameter type (e.g., `com.example.OrderRequest`).
|
||||||
|
3. **Data Linkage**: If a field from this payload is used in a `sendEvent(payload.getEvent())` call, mark the transition as "Dynamic based on Payload".
|
||||||
|
|
||||||
|
### Message Listeners
|
||||||
|
For `@RabbitListener` or `@JmsListener`:
|
||||||
|
1. Identify the parameter receiving the message body.
|
||||||
|
2. Extract its type and any `@Header` or `@Payload` annotations.
|
||||||
|
|
||||||
|
## 2. Interceptor and Filter Analysis
|
||||||
|
Interceptors often perform cross-cutting logic (authentication, context injection) that affects how state machines are driven.
|
||||||
|
|
||||||
|
### Discovery Strategy
|
||||||
|
1. **Find Interceptors**: Scan for classes implementing `HandlerInterceptor` or `WebRequestInterceptor`.
|
||||||
|
2. **Registration Mapping**: Look for `WebMvcConfigurer.addInterceptors()` calls to identify which URL patterns each interceptor applies to.
|
||||||
|
3. **Context Injection**: Analyze if the interceptor modifies the request (e.g., `request.setAttribute("userContext", ...)`) or populates a `ThreadLocal` (like `SecurityContextHolder`).
|
||||||
|
|
||||||
|
### Linking to State Machines
|
||||||
|
If a controller uses a value from a known Interceptor-injected context to decide which event to send or which state machine to restore, we can link the Interceptor's logic to the State Machine flow.
|
||||||
|
|
||||||
|
## 3. Implementation in `EnrichmentService`
|
||||||
|
We will introduce a `ContextEnricher` that:
|
||||||
|
1. Builds a map of **Active Interceptors** per path.
|
||||||
|
2. Augments `EntryPoint` metadata with **Payload Type** information.
|
||||||
|
|
||||||
|
## 4. Modeling Updates
|
||||||
|
Update `EntryPoint` and `TriggerPoint` to include `payloadType` and `interceptors`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record EntryPoint(
|
||||||
|
// ... existing fields ...
|
||||||
|
String payloadType,
|
||||||
|
List<String> activeInterceptors
|
||||||
|
) {}
|
||||||
|
```
|
||||||
63
plan-extended-anaylis/architecture.md
Normal file
63
plan-extended-anaylis/architecture.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Decoupled Analysis Architecture: The "Modular Provider" Approach
|
||||||
|
|
||||||
|
## Core Philosophy
|
||||||
|
We decouple the **State Machine Definition** (what the machine IS) from the **Codebase Intelligence** (how the machine IS USED). This allows for swappable analysis tools and prevents the state machine parser from becoming a monolithic "all-knowing" beast.
|
||||||
|
|
||||||
|
## 1. The Intelligence Interface (The Bridge)
|
||||||
|
We introduce an abstraction layer that provides information about the code without tying us to a specific implementation (like JDT).
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface CodebaseIntelligenceProvider {
|
||||||
|
// Event Triggers
|
||||||
|
List<TriggerPoint> findTriggerPoints();
|
||||||
|
|
||||||
|
// Entry Points
|
||||||
|
List<EntryPoint> findEntryPoints();
|
||||||
|
|
||||||
|
// Call Graphs
|
||||||
|
List<CallChain> traceCallChain(MethodReference target);
|
||||||
|
|
||||||
|
// Resolution
|
||||||
|
String resolveValue(ExpressionReference ref);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Swappable Implementations
|
||||||
|
This allows us to offer different "tiers" of analysis or even different tools:
|
||||||
|
- **JdtProvider (Current)**: High-precision, understands hierarchy, but heavy.
|
||||||
|
- **RegexProvider**: Lightweight, fast, good for simple projects or quick scans.
|
||||||
|
- **ExternalLspProvider (Future)**: Could query a running Language Server (like Eclipse JDT.LS) for ultra-accurate cross-project resolution.
|
||||||
|
|
||||||
|
## 3. The "State Machine Model" is the Sink
|
||||||
|
The State Machine Exporter now becomes a "Consumer" of intelligence.
|
||||||
|
1. **SM Parser**: Extracts the core states and transitions.
|
||||||
|
2. **Intelligence Service**: Discovers where events are triggered.
|
||||||
|
3. **Correlation Engine**: Merges the two models based on Event IDs.
|
||||||
|
|
||||||
|
## 4. Benefits
|
||||||
|
- **Testability**: We can mock the `CodebaseIntelligenceProvider` to test state machine rendering without needing a full Java project on disk.
|
||||||
|
- **Maintainability**: Adding support for a new framework (e.g., Micronaut) only requires updating the `IntelligenceProvider`, not touching the core state machine logic.
|
||||||
|
- **Performance**: We can run the SM Parser and the Intelligence Scanner in parallel since they are now independent.
|
||||||
|
|
||||||
|
## 5. Monorepo and Multi-Module Support
|
||||||
|
Applications are often split into multiple modules (e.g., `core`, `api`, `workers`).
|
||||||
|
- **Workspace Scanning**: The analyzer should treat the entire monorepo as a single codebase context.
|
||||||
|
- **Source Tracking**: Each metadata item (`TriggerPoint`, `EntryPoint`) includes a `sourceModule` identifier to show exactly where it was found.
|
||||||
|
- **Cross-Module Resolution**: Properties defined in one module's `application.yml` and used in another should be resolved globally.
|
||||||
|
- **Internal Dependency Following**: If the analyzer finds a call to a method in another module, it should continue the call-chain analysis into that module's source.
|
||||||
|
|
||||||
|
## Challenges
|
||||||
|
- **Multiple State Machines**: How to know which `StateMachine` instance is being used?
|
||||||
|
- Initial heuristic: If there's only one, assume it's that one.
|
||||||
|
- If multiple, check generic types `StateMachine<S, E>` or variable names.
|
||||||
|
- **Indirect Calls**: Method `A` (Controller) calls `B` (Service), and `B` calls `sendEvent`.
|
||||||
|
- Static analysis might need to follow call graphs.
|
||||||
|
- Use JDT's cross-reference capabilities if possible, or build a simple call graph.
|
||||||
|
- **Inheritance**:
|
||||||
|
- Controllers might extend base classes with common mappings.
|
||||||
|
- State machine configurations already handle inheritance; Trigger detection should too.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. Create a PoC `TriggerDetector` for Spring MVC.
|
||||||
|
2. Integrate `TriggerAggregator` into the main analysis pipeline.
|
||||||
|
3. Update the `Exporter` to visualize these links.
|
||||||
40
plan-extended-anaylis/decoupled_logic.md
Normal file
40
plan-extended-anaylis/decoupled_logic.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# The Analysis Contract: Decoupling Metadata from Implementation
|
||||||
|
|
||||||
|
## 1. Unified Model
|
||||||
|
Instead of each parser having its own internal model, we move to a "Context-Rich Model".
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record CodebaseMetadata(
|
||||||
|
List<TriggerPoint> triggers,
|
||||||
|
List<EntryPoint> entryPoints,
|
||||||
|
Map<String, String> properties,
|
||||||
|
CallGraph callGraph
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Intelligence Provider Lifecycle
|
||||||
|
1. **Initialize**: Provide root paths and configuration.
|
||||||
|
2. **Scan**: The provider builds its internal index (e.g., JDT AST, Regex indices).
|
||||||
|
3. **Extract**: The provider returns a `CodebaseMetadata` object.
|
||||||
|
4. **Dispose**: Cleanup resources.
|
||||||
|
|
||||||
|
## 3. Tool-Specific Strengths
|
||||||
|
|
||||||
|
### JDT Provider (Advanced)
|
||||||
|
- **Strengths**: True semantic understanding, inheritance resolution, precise constant lookups.
|
||||||
|
- **Usage**: Deep analysis where we need to know that `BaseService.notify()` is actually what is being called in `ChildService`.
|
||||||
|
|
||||||
|
### Regex / Tree-Sitter Provider (Light)
|
||||||
|
- **Strengths**: Extremely fast, works even on broken code (doesn't need full classpath), very low memory overhead.
|
||||||
|
- **Usage**: Large monorepos where full JDT analysis would take minutes.
|
||||||
|
|
||||||
|
## 4. Aggregation Logic
|
||||||
|
The `IntelligenceService` can actually combine results from multiple providers!
|
||||||
|
- Use JDT for the State Machine Config (where we need high precision).
|
||||||
|
- Use Regex for a quick scan of 1000+ controllers to find mapping patterns.
|
||||||
|
|
||||||
|
## 5. Refactoring Strategy
|
||||||
|
1. **Extract**: Move current JDT-based transition parsing into a `JdtStateMachineProvider`.
|
||||||
|
2. **Define**: Create the `CodebaseMetadata` model.
|
||||||
|
3. **Bridge**: Update the `ExportService` to accept both a `StateMachineModel` and a `CodebaseMetadata` object.
|
||||||
|
4. **Implement**: Create the `CodebaseIntelligenceProvider` interface and its first implementation (wrapping the existing JDT logic).
|
||||||
67
plan-extended-anaylis/enrichment_strategy.md
Normal file
67
plan-extended-anaylis/enrichment_strategy.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Low-Impact Abstraction: The Enrichment Pipeline
|
||||||
|
|
||||||
|
## 1. Goal
|
||||||
|
Support enhanced codebase analysis (Endpoints, Listeners, etc.) without a massive refactor of the existing JDT state machine parsers.
|
||||||
|
|
||||||
|
## 2. The Core Model: `AnalysisResult`
|
||||||
|
We wrap the existing output into a container that can hold optional metadata.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class AnalysisResult {
|
||||||
|
// Current Core Data
|
||||||
|
private final String name;
|
||||||
|
private final List<Transition> transitions;
|
||||||
|
private final Set<String> startStates;
|
||||||
|
private final Set<String> endStates;
|
||||||
|
|
||||||
|
// Optional Enhanced Data
|
||||||
|
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
||||||
|
|
||||||
|
public AnalysisResult(String name, List<Transition> transitions, Set<String> startStates, Set<String> endStates) {
|
||||||
|
this.name = name;
|
||||||
|
this.transitions = transitions;
|
||||||
|
this.startStates = startStates;
|
||||||
|
this.endStates = endStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMetadata(CodebaseMetadata metadata) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. The "Enricher" Abstraction
|
||||||
|
Instead of modifying the parser, we "enrich" the result after it's produced.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface AnalysisEnricher {
|
||||||
|
/**
|
||||||
|
* Examines the core analysis result and the codebase context
|
||||||
|
* to add extra metadata (triggers, endpoints, etc.).
|
||||||
|
*/
|
||||||
|
void enrich(AnalysisResult result, CodebaseContext context);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Implementation Tiers
|
||||||
|
|
||||||
|
### Tier 1: Current State (No-Op)
|
||||||
|
The `ExportService` calls `enrich()`, but since no enrichers are registered, it just returns the core data. This is zero-impact refactoring.
|
||||||
|
|
||||||
|
### Tier 2: Enhanced Analysis
|
||||||
|
Once we implement `SpringMvcEnricher` or `RabbitMqEnricher`, we register them. They will:
|
||||||
|
1. Scan the `CodebaseContext`.
|
||||||
|
2. Find `TriggerPoints`.
|
||||||
|
3. Link them to the `transitions` already present in the `AnalysisResult`.
|
||||||
|
4. Update the `metadata` field.
|
||||||
|
|
||||||
|
## 5. Why this is "Good":
|
||||||
|
- **Non-Breaking**: `StateMachineAggregator` remains untouched.
|
||||||
|
- **Pluggable**: You can add/remove "Intelligence" without changing the core parser.
|
||||||
|
- **Future-Proof**: If you switch from JDT to another tool for code analysis, you only change the `Enricher` implementation.
|
||||||
|
|
||||||
|
## 6. Refactoring Steps
|
||||||
|
1. **Create `AnalysisResult`** and **`CodebaseMetadata`** classes.
|
||||||
|
2. **Update `ExportService.generateOutputs`** to take an `AnalysisResult` object instead of 4 separate parameters.
|
||||||
|
3. **Introduce `EnrichmentService`** that holds a list of `AnalysisEnricher`s.
|
||||||
|
4. **Modify `ExportService`** to call `enrichmentService.enrich(result, context)` before passing the result to exporters.
|
||||||
92
plan-extended-anaylis/generic_extensibility.md
Normal file
92
plan-extended-anaylis/generic_extensibility.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Generic and Extensible Endpoint Finder
|
||||||
|
|
||||||
|
## The Need for Genericity
|
||||||
|
Applications use various frameworks and patterns to trigger state machine events. While Spring MVC and Kafka are common, others like Micronuat, Jakarta EE, or custom internal event buses might be used.
|
||||||
|
|
||||||
|
## Configuration-Driven Detection
|
||||||
|
We can allow users to define "Entry Point" patterns via a configuration file (e.g., `analysis-config.json`).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entryPoints": [
|
||||||
|
{
|
||||||
|
"name": "SpringMVC",
|
||||||
|
"classAnnotations": ["RestController", "Controller"],
|
||||||
|
"methodAnnotations": ["PostMapping", "GetMapping", "PutMapping", "DeleteMapping", "RequestMapping"],
|
||||||
|
"metadataExtractors": {
|
||||||
|
"path": ["value", "path"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kafka",
|
||||||
|
"methodAnnotations": ["KafkaListener"],
|
||||||
|
"metadataExtractors": {
|
||||||
|
"topic": ["topics", "topicPartitions"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Abstract EntryPoint Detector
|
||||||
|
A generic detector that uses the above configuration to identify entry points.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ConfigurableTriggerDetector implements TriggerDetector {
|
||||||
|
private final AnalysisConfig config;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TriggerPoint> detect(CompilationUnit cu, CodebaseContext context) {
|
||||||
|
// 1. Find all classes
|
||||||
|
// 2. If class has any of config.classAnnotations
|
||||||
|
// 3. Scan methods for any of config.methodAnnotations
|
||||||
|
// 4. If found, look for sendEvent calls inside the method or its call tree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Indirect Flow Detection (Call Tree Analysis)
|
||||||
|
Oftentimes, the `sendEvent` call is not directly in the controller method but in a service called by the controller.
|
||||||
|
|
||||||
|
### Simple Call Tree Analysis
|
||||||
|
1. Find all methods that call `sendEvent(E)`. Let's call them "Direct Triggers".
|
||||||
|
2. Find all methods that call "Direct Triggers".
|
||||||
|
3. Recursively build this "Trigger Path".
|
||||||
|
4. If a "Trigger Path" starts at an "Entry Point" (Controller/Listener), we have a complete flow.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
`OrderController.submitOrder()` -> `OrderService.processOrder()` -> `StateMachine.sendEvent(SUBMIT)`
|
||||||
|
|
||||||
|
Analysis:
|
||||||
|
1. `OrderService.processOrder()` is a Direct Trigger for `SUBMIT`.
|
||||||
|
2. `OrderController.submitOrder()` calls `OrderService.processOrder()`.
|
||||||
|
3. `OrderController.submitOrder()` is an Entry Point (annotated with `@PostMapping`).
|
||||||
|
4. Result: `POST /order` -> `SUBMIT` transition.
|
||||||
|
|
||||||
|
## Handling Reactive Chains Statically
|
||||||
|
In WebFlux/Project Reactor, the logic is decoupled into a pipeline. Static analysis must "connect the dots" across operators.
|
||||||
|
|
||||||
|
**Strategy**:
|
||||||
|
1. When analyzing a method, identify if it returns a reactive type (`Mono`, `Flux`).
|
||||||
|
2. If so, inspect all lambdas within that method.
|
||||||
|
3. If a lambda calls `sendEvent`, treat the method as a "Trigger Point".
|
||||||
|
4. If the method is also an "Entry Point" (via annotations or `RouterFunction`), we have a direct reactive flow.
|
||||||
|
|
||||||
|
## Supporting Inheritance in Listeners
|
||||||
|
Often, base classes or interfaces define generic listeners:
|
||||||
|
```java
|
||||||
|
public interface BaseConsumer<T> {
|
||||||
|
@RabbitListener(queues = "${app.queue}")
|
||||||
|
void handle(T payload);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The `CodebaseContext` must resolve the `${app.queue}` if possible (via `application.properties` scanning) or at least note that it's a parameterized listener.
|
||||||
|
|
||||||
|
## Advanced Call Graph: The "Data Flow"
|
||||||
|
For a truly detailed analysis, we should track NOT just method calls, but also how the "Event" object is passed around.
|
||||||
|
- Is the event hardcoded? `sm.sendEvent(Events.CREATE)`
|
||||||
|
- Is it derived from a payload field? `sm.sendEvent(payload.getEvent())`
|
||||||
|
- Is it mapped via a `switch` or `Map`?
|
||||||
|
|
||||||
|
**Static Mapping Inference**:
|
||||||
|
If we see a `switch` statement on a payload field that leads to different `sendEvent` calls, we can report multiple possible flows from the same endpoint.
|
||||||
102
plan-extended-anaylis/implementation_details.md
Normal file
102
plan-extended-anaylis/implementation_details.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Implementation Details: Trigger Detection with JDT
|
||||||
|
|
||||||
|
## 1. Finding `sendEvent` Calls
|
||||||
|
We can use an `ASTVisitor` to find all `MethodInvocation` nodes.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class SendEventVisitor extends ASTVisitor {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if ("sendEvent".equals(node.getName().getIdentifier())) {
|
||||||
|
// Found a call!
|
||||||
|
// 1. Extract event (first argument)
|
||||||
|
// 2. Identify the enclosing method and class
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Challenges in Event Extraction
|
||||||
|
Events can be:
|
||||||
|
- Enum constants: `Events.SUBMIT`
|
||||||
|
- Strings: `"SUBMIT"`
|
||||||
|
- Variables: `sm.sendEvent(eventFromPayload)` (Hard to resolve statically)
|
||||||
|
|
||||||
|
We should reuse `CodebaseContext.resolveState` logic (which is basically resolving an expression to a value/fqn).
|
||||||
|
|
||||||
|
## 2. Identifying Enclosing Context
|
||||||
|
Once a `sendEvent` is found, we can traverse up the AST to find the `MethodDeclaration` and `TypeDeclaration`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
// parent is now the MethodDeclaration
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Extracting Annotations
|
||||||
|
From `MethodDeclaration`, we can check for mappings:
|
||||||
|
- `@PostMapping`, `@GetMapping`, etc.
|
||||||
|
- `@KafkaListener`: Extract `topics`, `groupId`.
|
||||||
|
- `@RabbitListener`: Extract `queues`, `bindings` (Exchange/RoutingKey).
|
||||||
|
|
||||||
|
From `TypeDeclaration`, we can check for:
|
||||||
|
- `@RestController`, `@Controller`
|
||||||
|
- `@RequestMapping` at class level (to get base path)
|
||||||
|
|
||||||
|
## 3. Specialized WebFlux Analysis
|
||||||
|
|
||||||
|
### Annotation-based WebFlux
|
||||||
|
This is largely identical to Spring MVC. The challenge is if the `sendEvent` is wrapped in a reactive operator.
|
||||||
|
```java
|
||||||
|
public Mono<Void> submit(Order order) {
|
||||||
|
return service.save(order)
|
||||||
|
.doOnNext(o -> stateMachine.sendEvent(Events.SUBMIT))
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Static Strategy**: We need to look inside `LambdaExpression` nodes passed to reactive operators (`doOnNext`, `flatMap`, `map`, `subscribe`). The `ASTVisitor` should traverse into these lambdas.
|
||||||
|
|
||||||
|
### Functional WebFlux (RouterFunctions)
|
||||||
|
Functional endpoints are often defined as Beans returning `RouterFunction`.
|
||||||
|
```java
|
||||||
|
@Bean
|
||||||
|
public RouterFunction<ServerResponse> route(OrderHandler handler) {
|
||||||
|
return RouterFunctions.route(POST("/orders"), handler::submitOrder);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Static Strategy**:
|
||||||
|
1. Find methods returning `RouterFunction`.
|
||||||
|
2. Analyze the `MethodInvocation` chain (`route`, `andRoute`, `nest`).
|
||||||
|
3. Extract the URI pattern and the `HandlerFunction` reference.
|
||||||
|
4. If the handler is a method reference (`handler::submitOrder`), link it to the corresponding `MethodDeclaration`.
|
||||||
|
|
||||||
|
## 4. Specialized RabbitMQ Analysis
|
||||||
|
`@RabbitListener` can be complex:
|
||||||
|
```java
|
||||||
|
@RabbitListener(bindings = @QueueBinding(
|
||||||
|
value = @Queue(value = "orderQueue", durable = "true"),
|
||||||
|
exchange = @Exchange(value = "orderExchange"),
|
||||||
|
key = "order.created"
|
||||||
|
))
|
||||||
|
public void onOrder(Order order) { ... }
|
||||||
|
```
|
||||||
|
**Static Strategy**:
|
||||||
|
1. Find `@RabbitListener`.
|
||||||
|
2. If it has `bindings`, drill down into `@QueueBinding`, `@Queue`, `@Exchange` to extract the topology.
|
||||||
|
3. If it only has `queues`, resolve the queue name (might be a SpEL expression or property placeholder, which we can try to resolve or just keep as-is).
|
||||||
|
|
||||||
|
## 5. Indirect Flow Detection (The "Service Link")
|
||||||
|
If the project has multiple state machines, we need to know which one is being targeted.
|
||||||
|
Usually, this is done via:
|
||||||
|
- Autowiring by type: `StateMachine<States, Events> sm;`
|
||||||
|
- Autowiring by name: `@Qualifier("mySm") StateMachine sm;`
|
||||||
|
|
||||||
|
We can look at the fields of the class where `sendEvent` is called.
|
||||||
|
|
||||||
|
## 6. Output Enhancement
|
||||||
|
The `Exporter` should be modified to:
|
||||||
|
- In DOT: Add nodes for Endpoints/Listeners and link them to the Events/Transitions.
|
||||||
|
- In SCXML: Add metadata to transitions.
|
||||||
73
plan-extended-anaylis/instance_identification.md
Normal file
73
plan-extended-anaylis/instance_identification.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# State Machine Instance Identification & Persistence Mapping
|
||||||
|
|
||||||
|
## 1. The Multi-SM Problem
|
||||||
|
In large applications, multiple State Machines often coexist. A "Trigger Point" (like a REST Controller) must be linked to the correct State Machine definition.
|
||||||
|
|
||||||
|
**Common Identification Patterns**:
|
||||||
|
- **Unique Types**: `StateMachine<OrderState, OrderEvent>` vs `StateMachine<UserState, UserEvent>`.
|
||||||
|
- **Bean Qualifiers**: `@Qualifier("orderStateMachine")`.
|
||||||
|
- **Factory IDs**: `factory.getStateMachine("order-123")`.
|
||||||
|
- **Persistence Restoration**: `persister.restore(sm, orderId)`.
|
||||||
|
|
||||||
|
## 2. Analysis Strategy: "Instance Tracking"
|
||||||
|
|
||||||
|
### 1. Definition Discovery (Existing)
|
||||||
|
Identify all configurations (classes with `@EnableStateMachineFactory` or `@EnableStateMachine`). Store their **Bean Names** and **Generic Types**.
|
||||||
|
|
||||||
|
### 2. Dependency Analysis
|
||||||
|
When a class (e.g., `OrderController`) uses a state machine:
|
||||||
|
1. **Identify the Field/Parameter**: Look for `StateMachine<S, E>`.
|
||||||
|
2. **Resolve Generic Types**: Match `S` and `E` against known SM definitions.
|
||||||
|
3. **Resolve Qualifiers**: Check for `@Qualifier` or variable names that match a SM bean name.
|
||||||
|
|
||||||
|
### 3. Loading & Persistence Analysis
|
||||||
|
A dedicated "Loading Detector" will look for persistence logic.
|
||||||
|
|
||||||
|
**Pattern: Persister Restore**
|
||||||
|
```java
|
||||||
|
persister.restore(stateMachine, id);
|
||||||
|
stateMachine.sendEvent(E);
|
||||||
|
```
|
||||||
|
**Static Strategy**:
|
||||||
|
- Find calls to `Persister.restore(sm, ...)` or `PersistStateChangeListener`.
|
||||||
|
- Link the variable `sm` to the restoration event.
|
||||||
|
- Mark the `TriggerPoint` as "Restored from Persistence".
|
||||||
|
|
||||||
|
**Pattern: Factory Creation**
|
||||||
|
```java
|
||||||
|
StateMachine sm = factory.getStateMachine(smId);
|
||||||
|
```
|
||||||
|
**Static Strategy**:
|
||||||
|
- Find `factory.getStateMachine(...)`.
|
||||||
|
- If the argument is a literal (e.g., `"order"`), map it to the SM definition named "order".
|
||||||
|
|
||||||
|
## 3. Implementation: `InstanceIdentifier`
|
||||||
|
We will introduce an `InstanceIdentifier` that works alongside the `ValueResolver`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class InstanceIdentifier {
|
||||||
|
public StateMachineReference identify(VariableDeclaration var, CodebaseContext context) {
|
||||||
|
// 1. Check type generics
|
||||||
|
// 2. Check @Qualifier
|
||||||
|
// 3. Trace back to factory or persister calls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Modeling in `AnalysisResult`
|
||||||
|
The `TriggerPoint` will be enhanced with a `stateMachineId` or `configFqn` field.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record TriggerPoint(
|
||||||
|
String className,
|
||||||
|
String methodName,
|
||||||
|
String event,
|
||||||
|
String stateMachineId, // Links back to the specific SM
|
||||||
|
boolean isRestoredFromPersistence,
|
||||||
|
Map<String, String> metadata
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Challenges
|
||||||
|
- **Generic Controllers**: A single base controller that handles multiple SMs via generics. We might need to report "Multiple Potential SMs".
|
||||||
|
- **Dynamic Factory IDs**: `factory.getStateMachine(payload.getType())`. Hard to resolve statically without data flow analysis.
|
||||||
55
plan-extended-anaylis/polyglot_support.md
Normal file
55
plan-extended-anaylis/polyglot_support.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Polyglot Analysis: Supporting Mixed Java and Kotlin
|
||||||
|
|
||||||
|
## 1. The Need for Language Abstraction
|
||||||
|
Professional JVM codebases are increasingly "Mixed Mode" (Java + Kotlin). Tying our analysis logic directly to JDT (Java AST) creates a bottleneck for Kotlin support.
|
||||||
|
|
||||||
|
## 2. The "Driver" Architecture
|
||||||
|
We decouple the **Intelligence Logic** (e.g., "Find all REST controllers") from the **AST Parser** (e.g., "Walk the JDT tree").
|
||||||
|
|
||||||
|
### 1. Language Driver Interface
|
||||||
|
```java
|
||||||
|
public interface LanguageDriver {
|
||||||
|
boolean supports(Path filePath);
|
||||||
|
|
||||||
|
// Unified Extraction Methods
|
||||||
|
List<RawClassMetadata> extractClasses(Path filePath);
|
||||||
|
List<RawMethodMetadata> extractMethods(RawClassMetadata cls);
|
||||||
|
List<RawInvocationMetadata> findInvocations(RawMethodMetadata method, String targetMethodName);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Unified Metadata Model (Intermediate Representation)
|
||||||
|
We move to a "Generic JVM Model" before performing the final state machine mapping.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public record RawClassMetadata(
|
||||||
|
String fqn,
|
||||||
|
String language, // "java", "kotlin"
|
||||||
|
List<String> annotations,
|
||||||
|
String superclass,
|
||||||
|
List<String> interfaces
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public record RawMethodMetadata(
|
||||||
|
String name,
|
||||||
|
List<String> annotations,
|
||||||
|
String returnType,
|
||||||
|
List<String> parameterTypes
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Polyglot Analysis Flow
|
||||||
|
1. **File Discovery**: Find all `.java` and `.kt` files.
|
||||||
|
2. **Driver Assignment**: Delegate each file to the appropriate `LanguageDriver`.
|
||||||
|
3. **Cross-Language Resolution**: The `CodebaseContext` stores metadata for ALL classes.
|
||||||
|
- A Java controller calling a Kotlin service is resolved by looking up the Kotlin class's unified metadata.
|
||||||
|
4. **Enricher Execution**: Enrichers (like `SpringMvcEnricher`) now operate on the **Unified Metadata Model**, making them language-agnostic!
|
||||||
|
|
||||||
|
## 4. Why this is "Future Proof":
|
||||||
|
- **Enrichers are written once**: The logic to find `@PostMapping` only needs to know how to query the `RawClassMetadata` annotations, not how to traverse JDT vs. Kotlin PSI.
|
||||||
|
- **Easy Expansion**: To support Scala, Groovy, or even a newer Java version, you just add a new `LanguageDriver`.
|
||||||
|
|
||||||
|
## 5. Refactoring Plan
|
||||||
|
1. **Define Intermediate Models**: Create `RawClassMetadata` and `RawMethodMetadata`.
|
||||||
|
2. **Extract JDT Logic**: Move the current JDT-specific code into `JavaLanguageDriver`.
|
||||||
|
3. **Kotlin PoC (Phase 12)**: Implement a basic `KotlinLanguageDriver` using Regex or a lightweight parser (like Tree-Sitter) to prove the abstraction works.
|
||||||
80
plan-extended-anaylis/property_resolution.md
Normal file
80
plan-extended-anaylis/property_resolution.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Spring Property and Value Resolution Strategy
|
||||||
|
|
||||||
|
## 1. The Challenge of "Injected" Metadata
|
||||||
|
In Spring, metadata (like Queue names, Topic names, or even Event names) is often not hardcoded but injected using various mechanisms. To provide an accurate map, the analyzer must resolve these values.
|
||||||
|
|
||||||
|
## 2. Supported Injection Patterns
|
||||||
|
|
||||||
|
### Field Injection
|
||||||
|
```java
|
||||||
|
@Value("${app.order-queue}")
|
||||||
|
private String orderQueue;
|
||||||
|
```
|
||||||
|
**Static Strategy**:
|
||||||
|
1. Scan all fields for `@Value`.
|
||||||
|
2. Map `FieldName` -> `Placeholder/Value`.
|
||||||
|
3. If `${...}` is found, resolve it via the `PropertyResolver`.
|
||||||
|
|
||||||
|
### Constructor and Setter Injection
|
||||||
|
```java
|
||||||
|
public OrderService(@Value("${app.event.submit}") String submitEvent) {
|
||||||
|
this.submitEvent = submitEvent;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Static Strategy**:
|
||||||
|
1. Scan constructor/method parameters for `@Value`.
|
||||||
|
2. Trace the assignment to a class field (`this.submitEvent = ...`).
|
||||||
|
3. Store the mapping for that field.
|
||||||
|
|
||||||
|
### @ConfigurationProperties
|
||||||
|
```java
|
||||||
|
@ConfigurationProperties(prefix = "app.messaging")
|
||||||
|
public class AppProps {
|
||||||
|
private String queueName;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Static Strategy**:
|
||||||
|
1. Identify `@ConfigurationProperties` classes.
|
||||||
|
2. Index their fields with the prefix (e.g., `app.messaging.queueName`).
|
||||||
|
3. When these beans are injected into a service, link the service's usage to the resolved property value.
|
||||||
|
|
||||||
|
## 3. Profile-Aware Property Resolver
|
||||||
|
A component dedicated to building a multi-dimensional map of available properties.
|
||||||
|
|
||||||
|
1. **File Scanning**: Parse `application.properties`, `application.yml`, and `bootstrap.yml`.
|
||||||
|
2. **Profile Identification**:
|
||||||
|
- From filenames: `application-{profile}.properties`.
|
||||||
|
- From YAML documents: `spring.config.activate.on-profile` (Spring Boot 2.4+).
|
||||||
|
3. **The Multi-Profile Map**:
|
||||||
|
- Instead of one map, we store: `Map<ProfileName, Map<Key, Value>>`.
|
||||||
|
- The "default" profile is the base.
|
||||||
|
|
||||||
|
## 4. Smart Value Propagation
|
||||||
|
When a trigger uses a property, we want to know it's profile-dependent.
|
||||||
|
|
||||||
|
**Example Trace**:
|
||||||
|
1. `application.yml` -> `app.queue: "orders"`
|
||||||
|
2. `application-prod.yml` -> `app.queue: "orders-prod"`
|
||||||
|
3. `TriggerPoint` stores:
|
||||||
|
- `placeholder: "${app.queue}"`
|
||||||
|
- `defaultResolvedValue: "orders"`
|
||||||
|
4. **Rendering Strategy**: The renderer can show "orders" by default, but provide a "Profile Toggle" to switch to "prod" and see the labels update to "orders-prod".
|
||||||
|
|
||||||
|
## 5. Metadata Retention
|
||||||
|
We should never "squash" profiles during analysis. The `CodebaseMetadata` will carry the full profile matrix to the exporter.
|
||||||
|
|
||||||
|
## 5. SpEL (Spring Expression Language) Lite
|
||||||
|
Full SpEL support is hard for static analysis, but we can support "Common Patterns":
|
||||||
|
- Simple bean property access: `#{myBean.name}`
|
||||||
|
- System properties: `#{systemProperties['user.dir']}`
|
||||||
|
- Ternary operators: `${app.enabled ? 'active' : 'inactive'}`
|
||||||
|
|
||||||
|
## 6. Constant Resolution
|
||||||
|
If an annotation uses a reference to a constant:
|
||||||
|
```java
|
||||||
|
@RabbitListener(queues = MyConstants.ORDER_QUEUE)
|
||||||
|
```
|
||||||
|
**Static Strategy**:
|
||||||
|
1. Use JDT to resolve the `QualifiedName` (`MyConstants.ORDER_QUEUE`).
|
||||||
|
2. Fetch the `TypeDeclaration` for `MyConstants`.
|
||||||
|
3. Find the `VariableDeclarationFragment` for `ORDER_QUEUE` and extract its literal initializer.
|
||||||
53
plan-extended-anaylis/string_resolution.md
Normal file
53
plan-extended-anaylis/string_resolution.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Advanced String and Variable Resolution Strategy
|
||||||
|
|
||||||
|
## 1. The Problem
|
||||||
|
Annotations often use non-literal values for metadata:
|
||||||
|
- **Constants**: `@PostMapping(ApiConstants.SUBMIT_PATH)`
|
||||||
|
- **Concatenation**: `@RequestMapping(BASE + "/orders")`
|
||||||
|
- **Inherited Variables**: Use of fields defined in base classes.
|
||||||
|
|
||||||
|
Simple `StringLiteral` extraction fails in these cases. We need a recursive **Expression Evaluator**.
|
||||||
|
|
||||||
|
## 2. Evaluation Logic
|
||||||
|
|
||||||
|
### Constant Resolution
|
||||||
|
If an expression is a `SimpleName` or `QualifiedName`:
|
||||||
|
1. Use `CodebaseContext.getTypeDeclaration()` to find the class where the variable is defined.
|
||||||
|
2. Search for the `VariableDeclarationFragment`.
|
||||||
|
3. If it has an initializer that is a literal or another evaluatable expression, resolve it.
|
||||||
|
|
||||||
|
### Concatenation (Infix Expressions)
|
||||||
|
If an expression is an `InfixExpression` with the `+` operator:
|
||||||
|
1. Recursively resolve the left operand.
|
||||||
|
2. Recursively resolve the right operand.
|
||||||
|
3. Concatenate the results if both are strings.
|
||||||
|
|
||||||
|
### Spring Expression Language (SpEL) in Annotations
|
||||||
|
For values like `@Value("#{systemProperties['path.base'] + '/api'}")`:
|
||||||
|
1. Identify the SpEL string.
|
||||||
|
2. Use a "Lite" SpEL parser to extract keys.
|
||||||
|
3. Match against the `profiles` map in `CodebaseMetadata`.
|
||||||
|
|
||||||
|
## 3. Implementation: `ValueResolver`
|
||||||
|
We will introduce a central `ValueResolver` utility.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ValueResolver {
|
||||||
|
public String resolveString(Expression expr, CodebaseContext context) {
|
||||||
|
if (expr instanceof StringLiteral sl) return sl.getLiteralValue();
|
||||||
|
if (expr instanceof InfixExpression ie) return resolveInfix(ie, context);
|
||||||
|
if (expr instanceof Name name) return resolveVariable(name, context);
|
||||||
|
// ... fallback to toString() or empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Challenges
|
||||||
|
- **Circular Dependencies**: A + B, where B = A + C. We need a "visited" set to prevent infinite recursion.
|
||||||
|
- **Runtime-only values**: Some values cannot be resolved statically (e.g., values from a DB). In these cases, we should return the placeholder or a "Runtime Value" marker.
|
||||||
|
- **Method Calls**: `getPath() + "/orders"`. Resolving method return values is complex; initially, we will only support simple getters returning literals.
|
||||||
|
|
||||||
|
## 5. Integration
|
||||||
|
1. Update `AstUtils` or create `ValueResolver`.
|
||||||
|
2. Refactor `SpringMvcEnricher`, `RabbitMqEnricher`, and `JmsEnricher` to use the resolver for all path/queue/destination fields.
|
||||||
|
3. Update `integration_test_state_machine` with complex path examples.
|
||||||
4
run_golden.gradle
Normal file
4
run_golden.gradle
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
task runGolden(type: JavaExec) {
|
||||||
|
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
||||||
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ rootProject.name = 'state machine exporter'
|
|||||||
|
|
||||||
include ':file_combine'
|
include ':file_combine'
|
||||||
include ':state_machine_exporter'
|
include ':state_machine_exporter'
|
||||||
|
include ':state_machine_exporter_html'
|
||||||
include ':state_machine_exporter_spring_based'
|
include ':state_machine_exporter_spring_based'
|
||||||
include ':state_machines:complex_state_machine'
|
include ':state_machines:complex_state_machine'
|
||||||
include ':state_machines:dynamic_state_machine'
|
include ':state_machines:dynamic_state_machine'
|
||||||
@@ -12,3 +13,9 @@ include ':state_machines:inheritance_extra_functions_state_machine'
|
|||||||
include ':state_machines:inheritance_extra_functions2_state_machine'
|
include ':state_machines:inheritance_extra_functions2_state_machine'
|
||||||
include ':state_machines:inheritance_extra_functions3_state_machine'
|
include ':state_machines:inheritance_extra_functions3_state_machine'
|
||||||
include ':state_machines:simple_state_machine'
|
include ':state_machines:simple_state_machine'
|
||||||
|
include ':state_machines:extended_analysis_sample'
|
||||||
|
include ':state_machines:inheritance_sample'
|
||||||
|
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'
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ dependencies {
|
|||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.1'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.1'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1'
|
||||||
|
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.46'
|
compileOnly 'org.projectlombok:lombok:1.18.46'
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.46'
|
annotationProcessor 'org.projectlombok:lombok:1.18.46'
|
||||||
@@ -45,8 +46,22 @@ dependencies {
|
|||||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:6.1.0'
|
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:6.1.0'
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:6.1.0'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:6.1.0'
|
||||||
testImplementation 'org.assertj:assertj-core:3.27.7'
|
testImplementation 'org.assertj:assertj-core:3.27.7'
|
||||||
|
|
||||||
|
implementation 'net.sourceforge.plantuml:plantuml:1.2024.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
systemProperty "updateGolden", System.getProperty("updateGolden")
|
||||||
|
}
|
||||||
|
|
||||||
|
task runGolden(type: JavaExec) {
|
||||||
|
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
||||||
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
|
}
|
||||||
|
|
||||||
|
task runGoldenFixed(type: JavaExec) {
|
||||||
|
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
||||||
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
|
workingDir = rootProject.projectDir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
# Interactive HTML Generator: Architecture & Flow Definition Format
|
|
||||||
|
|
||||||
Following the success of the interactive SVG PoC, this document outlines the proposed architecture for adding an **HTML Generator** to the State Machine Exporter, allowing users to define and visualize specific business flows dynamically.
|
|
||||||
|
|
||||||
## 1. The Flow Definition Format (JSON & JSON Schema)
|
|
||||||
|
|
||||||
To make the interactive HTML generator useful and intuitive, users need a way to define their business flows externally. Rather than reinventing the wheel, we looked at industry standards for defining graph paths, workflows, and execution traces:
|
|
||||||
* **Workflow Definitions:** Tools like *Amazon States Language (ASL)*, *SCXML*, and *XState* are highly structured but define the *entire* machine's logic. They are too heavy for simply defining a "path" through an existing machine.
|
|
||||||
* **Trace Formats:** *OpenTelemetry (OTel)* uses "Spans" to represent individual steps forming a "Trace". This is closer to what we want (a history of states).
|
|
||||||
* **Graph Formats:** The *JSON Graph Format (JGF)* standardizes graph nodes and edges representation but is geared towards raw data modeling rather than human-readable business flows.
|
|
||||||
|
|
||||||
**Conclusion on Format:** We will use a lightweight custom JSON format inspired by execution traces. It is designed to be highly readable for humans defining business flows, relying simply on ordered arrays of State names.
|
|
||||||
|
|
||||||
### JSON Schema for IDE Autocomplete
|
|
||||||
To make the format truly intuitive and prevent typos, we will publish a **JSON Schema** (`flow-schema.json`).
|
|
||||||
By referencing this schema, developers writing `flows.json` in IDEs like VS Code or IntelliJ will get **instant autocomplete, hover tooltips, and real-time validation** for fields like `id`, `title`, and `color`.
|
|
||||||
* *Future Enhancement:* The Java exporter could dynamically generate a project-specific JSON Schema on the fly that hardcodes the allowed `enum` values for the `path` array (e.g., restricted exactly to `"SUBMITTED"`, `"PAID"`, etc.), providing perfect autocomplete for state names!
|
|
||||||
|
|
||||||
### Proposed Schema (`flows.json`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"$schema": "./flow-schema.json",
|
|
||||||
"stateMachine": "click.kamil.examples.statemachine.simple.SimpleEnumStateMachineConfiguration",
|
|
||||||
"flows": [
|
|
||||||
{
|
|
||||||
"id": "flow-happy-path",
|
|
||||||
"title": "Happy Path (Success)",
|
|
||||||
"description": "The standard flow for a successful order: Submitted -> Paid -> Fulfilled.",
|
|
||||||
"color": "#32CD32",
|
|
||||||
"path": [
|
|
||||||
".start.",
|
|
||||||
"SUBMITTED",
|
|
||||||
"PAID",
|
|
||||||
"FULFILLED",
|
|
||||||
".end."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "flow-cancel-post-pay",
|
|
||||||
"title": "Cancel Post-Payment",
|
|
||||||
"description": "Order is canceled after payment.",
|
|
||||||
"color": "#FF6347",
|
|
||||||
"path": [
|
|
||||||
".start.",
|
|
||||||
"SUBMITTED",
|
|
||||||
"PAID",
|
|
||||||
"CANCELED",
|
|
||||||
".end."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Format Details:
|
|
||||||
* **`$schema`**: Points to the JSON schema for IDE tooling.
|
|
||||||
* **`stateMachine`**: Links these flows to a specific state machine configuration in the codebase.
|
|
||||||
* **`id`**: A unique identifier for the flow (used for DOM elements).
|
|
||||||
* **`title` & `description`**: Rendered in the HTML sidebar for the user.
|
|
||||||
* **`color` (Optional)**: Allows overriding the default highlight color (e.g., green for success, red for errors).
|
|
||||||
* **`path`**: An ordered array of **State Names** (acting like an OpenTelemetry trace).
|
|
||||||
* *Note on Edges:* The user only needs to define the *states* they pass through. The generator will automatically calculate the edges (transitions) between these states to highlight the lines. Special tokens like `.start.` and `.end.` map to the initial and terminal pseudo-states.
|
|
||||||
|
|
||||||
## 2. Robust Error Handling & Input Validation
|
|
||||||
|
|
||||||
Before generating the HTML, the exporter **must validate** that the provided `flows.json` is logically sound against the actual codebase AST. If a user defines a flow that isn't physically possible in the state machine, the tool must fail fast with highly descriptive errors.
|
|
||||||
|
|
||||||
### Validation Rules & Error Handling Strategy:
|
|
||||||
1. **Schema Validation (Syntax):**
|
|
||||||
* *Check:* Is the JSON structurally correct (required fields present, correct types)?
|
|
||||||
* *Error:* "Invalid format in flows.json: 'title' is required for flow 'flow-cancel-post-pay'."
|
|
||||||
2. **State Existence (Dangling Pointers):**
|
|
||||||
* *Check:* Every state listed in the `path` array must exist in the parsed `StateMachineModel`.
|
|
||||||
* *Error:* "Validation Error in 'Happy Path': State 'FULLFILLED' does not exist in StateMachine 'SimpleEnum'. Did you mean 'FULFILLED'?" (implementing a Levenshtein distance check for typos would be excellent here).
|
|
||||||
3. **Transition Validity (Impossible Paths):**
|
|
||||||
* *Check:* For every adjacent pair in the `path` array (e.g., `["SUBMITTED", "PAID"]`), the validator must check the `StateMachineModel.getTransitions()` to ensure a valid transition exists where `source == "SUBMITTED"` and `target == "PAID"`.
|
|
||||||
* *Error:* "Validation Error in 'Happy Path': No transition exists from 'PAID' to 'SUBMITTED'. Please check your state machine configuration."
|
|
||||||
4. **Ambiguous Transitions (Multiple Edges):**
|
|
||||||
* *Check:* If multiple transitions exist between the same two states (e.g., two different events trigger a move from A -> B).
|
|
||||||
* *Handling:* By default, highlight *all* edges between A and B to represent the general path.
|
|
||||||
* *Future Enhancement:* Expand the JSON format to allow specifying the exact `event` name in the path array (e.g., `["SUBMITTED", {"state": "PAID", "event": "PAY_CREDIT_CARD"}]`) to disambiguate.
|
|
||||||
|
|
||||||
## 3. HTML Generation Strategy
|
|
||||||
|
|
||||||
The HTML Exporter will be a new class implementing the `StateMachineExporter` interface (or a specialized decorator around the `PlantUml` exporter).
|
|
||||||
|
|
||||||
### Steps for Generation:
|
|
||||||
1. **Generate PlantUML SVG:** First, use the existing PlantUML engine to generate the raw SVG string in memory (using the modern CSS `<style>` approach we finalized).
|
|
||||||
2. **Parse `flows.json`:** Load the user-provided flow definitions.
|
|
||||||
3. **Validate Flows:** Run the "Flow Validator" against the AST `StateMachineModel`.
|
|
||||||
4. **Calculate Edges:** For each valid flow, convert the `path` array (States) into an `edges` array (Transitions) so the Javascript knows exactly which SVG arrows to highlight.
|
|
||||||
5. **Inject Template:** Load a base HTML template (similar to `interactive_poc2.html`).
|
|
||||||
6. **Data Binding:**
|
|
||||||
* Inject the raw `<svg>` string directly into the template (avoiding CORS issues).
|
|
||||||
* Inject the validated flow data into a `<script>` tag as a JSON object, or directly render the sidebar HTML elements with `data-*` attributes.
|
|
||||||
7. **Output:** Write the final standalone `.html` file to the output directory.
|
|
||||||
|
|
||||||
## 4. Advantages of this Architecture
|
|
||||||
* **Standalone Output:** The resulting HTML file is 100% self-contained. No server, no external CSS/JS, and no CORS issues. It can be directly emailed, attached to Jira tickets, or hosted on GitHub Pages.
|
|
||||||
* **CI/CD Integration:** Because validation is strictly tied to the AST, if a developer refactors the Java State Machine and breaks a documented "Flow", the CI pipeline will fail, ensuring interactive documentation never goes out of date.
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Thought Experiment: Interactive SVG Flow Highlighting
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The goal of this thought experiment is to explore the feasibility and usefulness of animating or interactively highlighting specific "flows" (paths composed of states and transitions) within a generated state machine diagram. For example, hovering over a label like "Successful Payment Flow" would highlight the `SUBMITTED -> PAID -> FULFILLED` path, dimming the rest of the diagram.
|
|
||||||
|
|
||||||
## Usefulness
|
|
||||||
This feature would be **highly useful** and would significantly elevate the value of the State Machine Exporter:
|
|
||||||
1. **Comprehension of Complex Machines:** In large diagrams (like `ComplexStateMachineConfig`), a static image becomes a "spaghetti" of crossing lines. Interactive highlighting allows developers to visually isolate a single business transaction (e.g., "Cancellation Flow", "Refund Flow") without losing the context of the whole machine.
|
|
||||||
2. **Documentation & Onboarding:** It turns a static architectural diagram into an interactive documentation tool, making it much easier for new team members to trace how an entity moves through its lifecycle.
|
|
||||||
3. **Debugging:** By defining flows based on logged events, a developer could "replay" a sequence of events and see exactly what path the state machine took.
|
|
||||||
|
|
||||||
## Feasibility
|
|
||||||
This is **entirely feasible** using the tools currently available in the project.
|
|
||||||
|
|
||||||
### The Technical Reality of PlantUML SVGs
|
|
||||||
When PlantUML generates an SVG (`plantuml -tsvg`), it embeds highly predictable metadata into the DOM structure:
|
|
||||||
- **States (Nodes):** PlantUML creates `<g>` elements for states. They typically have an `id` matching the state name (e.g., `id="FULFILLED"`) or prefixed with entity (e.g., `id="entity_SUBMITTED"`).
|
|
||||||
- **Transitions (Links):** PlantUML creates `<g>` elements for arrows with predictable IDs and data attributes. For example: `<g class="link" data-entity-1="SUBMITTED" data-entity-2="PAID" id="link_SUBMITTED_PAID">`.
|
|
||||||
|
|
||||||
Because these identifiers match the semantic names of the states in the Java/JSON model, mapping a logical "Flow" to visual SVG elements is straightforward.
|
|
||||||
|
|
||||||
## Proposed Technical Approach
|
|
||||||
|
|
||||||
To realize this, we would build a lightweight **Interactive HTML Exporter** (or augment the JSON exporter to generate a companion HTML file).
|
|
||||||
|
|
||||||
### 1. Data Structure (The "Flow" Definition)
|
|
||||||
We need a way to define flows. This could be passed via configuration or inferred from the model. A flow is essentially an array of IDs:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Successful Payment",
|
|
||||||
"elements": ["SUBMITTED", "link_SUBMITTED_PAID", "PAID", "link_PAID_FULFILLED", "FULFILLED"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. HTML/JS Wrapper
|
|
||||||
Instead of just outputting an `.svg` or `.png`, the exporter would output an `.html` file that contains:
|
|
||||||
1. **The inline SVG code** (generated via PlantUML).
|
|
||||||
2. **A Sidebar UI** containing a list of defined flows.
|
|
||||||
3. **Vanilla JavaScript** to handle the interactivity.
|
|
||||||
|
|
||||||
### 3. The JavaScript Logic (Proof of Concept)
|
|
||||||
The JS logic is remarkably simple. It listens for `mouseenter` events on the flow list:
|
|
||||||
1. **Dim Everything:** Add a class to the root SVG wrapper (e.g., `.flow-active`) that sets `opacity: 0.2` on all `<g>` tags.
|
|
||||||
2. **Highlight Flow:** Iterate through the `elements` array of the active flow. Find the SVG elements by their `id` (e.g., `document.getElementById('link_SUBMITTED_PAID')`) and add a `.highlighted` class.
|
|
||||||
3. **CSS Magic:**
|
|
||||||
```css
|
|
||||||
.diagram-container.flow-active svg g.highlighted {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
.diagram-container.flow-active svg g.highlighted path {
|
|
||||||
stroke: red !important;
|
|
||||||
stroke-width: 3px !important;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Limitations & Challenges
|
|
||||||
|
|
||||||
While feasible, there are a few edge cases to handle:
|
|
||||||
|
|
||||||
1. **Multiple Transitions Between Same States:** If there are two distinct transitions from `A` to `B` (e.g., triggered by different events), PlantUML might generate IDs like `link_A_B` and `link_A_B_1`. The JS logic would need to rely on the event stereotypes we added recently (e.g., `<<e_PAY>>`) to distinguish them. We could parse the SVG text nodes to find the event label, or rely on the custom CSS classes if PlantUML supports passing them through to SVG (currently, PlantUML inlines styles rather than keeping CSS classes for transitions).
|
|
||||||
2. **Layout Shifts:** We must rely purely on CSS styling (colors, opacities, stroke widths). We cannot animate the *position* or layout of the arrows because PlantUML hardcodes the exact SVG path coordinates.
|
|
||||||
3. **Standalone vs. Hosted:** Generating a single `.html` file is easy and portable. However, if this is meant to be embedded in a documentation site (like Confluence or Backstage), injecting custom JS/CSS along with raw SVG can sometimes run into sanitization or iframe limitations.
|
|
||||||
4. **Maintenance:** Any changes to how PlantUML generates its internal SVG IDs in future versions would break the JS highlighting logic.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
The idea is brilliant. It transforms the output from a static image into an interactive, exploratory tool. Since the `StateMachineModel` is already cleanly exported as JSON, a standalone frontend script could easily marry that JSON data with the PlantUML SVG output to create dynamic, highlightable documentation flows without requiring heavy backend changes.
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; display: flex; }
|
|
||||||
#sidebar { width: 300px; padding: 20px; background: #f4f4f4; }
|
|
||||||
#diagram { flex-grow: 1; padding: 20px; }
|
|
||||||
|
|
||||||
.flow-btn { padding: 10px; margin-bottom: 5px; cursor: pointer; background: #ddd; border: 1px solid #ccc; border-radius: 4px;}
|
|
||||||
.flow-btn:hover { background: #bdf; }
|
|
||||||
|
|
||||||
/* Dim everything by default when a flow is active */
|
|
||||||
.diagram-container.flow-active svg g {
|
|
||||||
opacity: 0.2;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlight active elements */
|
|
||||||
.diagram-container.flow-active svg g.highlighted {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make highlighted paths thicker/red */
|
|
||||||
.diagram-container.flow-active svg g.highlighted path,
|
|
||||||
.diagram-container.flow-active svg g.highlighted polygon {
|
|
||||||
stroke: red !important;
|
|
||||||
stroke-width: 3px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagram-container.flow-active svg g.highlighted rect,
|
|
||||||
.diagram-container.flow-active svg g.highlighted ellipse {
|
|
||||||
stroke: red !important;
|
|
||||||
stroke-width: 3px !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="sidebar">
|
|
||||||
<h3>Flows</h3>
|
|
||||||
<div class="flow-btn" data-flow='["SUBMITTED", "link_SUBMITTED_PAID", "PAID", "link_PAID_FULFILLED", "FULFILLED"]'>Flow: Successful Payment</div>
|
|
||||||
<div class="flow-btn" data-flow='["SUBMITTED", "link_SUBMITTED_CANCELED", "CANCELED"]'>Flow: Cancel Before Pay</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="diagram" class="diagram-container">
|
|
||||||
<!-- SVG will be injected here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Load SVG
|
|
||||||
fetch('SimpleEnumStateMachineConfiguration.svg')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(svg => {
|
|
||||||
document.getElementById('diagram').innerHTML = svg;
|
|
||||||
setupInteractivity();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupInteractivity() {
|
|
||||||
const container = document.getElementById('diagram');
|
|
||||||
|
|
||||||
document.querySelectorAll('.flow-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('mouseenter', (e) => {
|
|
||||||
container.classList.add('flow-active');
|
|
||||||
const flowIds = JSON.parse(e.target.getAttribute('data-flow'));
|
|
||||||
|
|
||||||
flowIds.forEach(id => {
|
|
||||||
// PlantUML sometimes prepends entity_
|
|
||||||
let el = document.getElementById(id) || document.getElementById('entity_' + id);
|
|
||||||
if(el) {
|
|
||||||
el.classList.add('highlighted');
|
|
||||||
} else {
|
|
||||||
// For links, fallback to data-attributes if id parsing fails
|
|
||||||
if (id.startsWith('link_')) {
|
|
||||||
const parts = id.split('_');
|
|
||||||
let link = document.querySelector(`g[data-entity-1="${parts[1]}"][data-entity-2="${parts[2]}"]`);
|
|
||||||
if (link) link.classList.add('highlighted');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
btn.addEventListener('mouseleave', () => {
|
|
||||||
container.classList.remove('flow-active');
|
|
||||||
document.querySelectorAll('.highlighted').forEach(el => el.classList.remove('highlighted'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,29 +0,0 @@
|
|||||||
digraph statemachine {
|
|
||||||
rankdir=LR;
|
|
||||||
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
|
|
||||||
edge [fontname="Arial", fontsize=10];
|
|
||||||
|
|
||||||
SUBMITTED_choice [shape=diamond, label="", fillcolor="blue", width=0.2, height=0.2];
|
|
||||||
SUBMITTED -> SUBMITTED_choice [style=dotted, color="blue"];
|
|
||||||
PAID_choice [shape=diamond, label="", fillcolor="green", width=0.2, height=0.2];
|
|
||||||
PAID -> PAID_choice [style=dotted, color="green"];
|
|
||||||
_start [shape=circle, label="", fillcolor=black, width=0.1];
|
|
||||||
_start -> SUBMITTED;
|
|
||||||
CANCELED [fillcolor=lightgray];
|
|
||||||
FULFILLED [fillcolor=lightgray];
|
|
||||||
SUBMITTED -> PAID [label="OrderEvents.PAY", style="solid", color="black"];
|
|
||||||
PAID -> FULFILLED [label="OrderEvents.FULFILL", style="solid", color="black"];
|
|
||||||
SUBMITTED -> CANCELED [label="OrderEvents.CANCEL [λ]", style="solid", color="black"];
|
|
||||||
PAID -> CANCELED [label="OrderEvents.CANCEL", style="solid", color="black"];
|
|
||||||
SUBMITTED -> SUBMITTED [label="OrderEvents.ABCD", style="solid", color="black"];
|
|
||||||
SUBMITTED_choice -> PAID2 [label="[λ] (order=0)", style="solid", color="blue"];
|
|
||||||
SUBMITTED_choice -> PAID3 [label="(order=1)", style="solid", color="blue"];
|
|
||||||
PAID_choice -> PAID1 [label="[λ] (order=0)", style="solid", color="green"];
|
|
||||||
PAID_choice -> PAID2 [label="[guard1] (order=1)", style="solid", color="green"];
|
|
||||||
PAID_choice -> HAPPEN [label="[λ] (order=2)", style="solid", color="green"];
|
|
||||||
PAID_choice -> PAID3 [label="(order=3)", style="solid", color="green"];
|
|
||||||
PAID2 -> CANCELED [style="solid", color="black"];
|
|
||||||
PAID3 -> CANCELED [style="solid", color="black"];
|
|
||||||
PAID1 -> PAID1 [label="OrderEvents.ABCD / λ, action2", style="solid", color="black"];
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="SUBMITTED">
|
|
||||||
<state id="PAID1">
|
|
||||||
<transition target="PAID1" event="OrderEvents.ABCD"/>
|
|
||||||
</state>
|
|
||||||
<state id="SUBMITTED">
|
|
||||||
<transition target="PAID" event="OrderEvents.PAY"/>
|
|
||||||
<transition target="CANCELED" event="OrderEvents.CANCEL" cond="λ"/>
|
|
||||||
<transition target="SUBMITTED" event="OrderEvents.ABCD"/>
|
|
||||||
<transition target="PAID2" cond="λ">
|
|
||||||
<!-- order=0 -->
|
|
||||||
</transition>
|
|
||||||
<transition target="PAID3">
|
|
||||||
<!-- order=1 -->
|
|
||||||
</transition>
|
|
||||||
</state>
|
|
||||||
<state id="PAID">
|
|
||||||
<transition target="FULFILLED" event="OrderEvents.FULFILL"/>
|
|
||||||
<transition target="CANCELED" event="OrderEvents.CANCEL"/>
|
|
||||||
<transition target="PAID1" cond="λ">
|
|
||||||
<!-- order=0 -->
|
|
||||||
</transition>
|
|
||||||
<transition target="PAID2" cond="guard1">
|
|
||||||
<!-- order=1 -->
|
|
||||||
</transition>
|
|
||||||
<transition target="HAPPEN" cond="λ">
|
|
||||||
<!-- order=2 -->
|
|
||||||
</transition>
|
|
||||||
<transition target="PAID3">
|
|
||||||
<!-- order=3 -->
|
|
||||||
</transition>
|
|
||||||
</state>
|
|
||||||
<state id="CANCELED">
|
|
||||||
</state>
|
|
||||||
<state id="FULFILLED">
|
|
||||||
</state>
|
|
||||||
<state id="PAID3">
|
|
||||||
<transition target="CANCELED"/>
|
|
||||||
</state>
|
|
||||||
<state id="HAPPEN">
|
|
||||||
</state>
|
|
||||||
<state id="PAID2">
|
|
||||||
<transition target="CANCELED"/>
|
|
||||||
</state>
|
|
||||||
</scxml>
|
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package click.kamil.springstatemachineexporter;
|
package click.kamil.springstatemachineexporter;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
|
||||||
import click.kamil.springstatemachineexporter.exporter.Dot;
|
import click.kamil.springstatemachineexporter.exporter.Dot;
|
||||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
||||||
import click.kamil.springstatemachineexporter.exporter.PlantUml;
|
import click.kamil.springstatemachineexporter.exporter.PlantUml;
|
||||||
@@ -8,10 +9,19 @@ import click.kamil.springstatemachineexporter.command.ExporterCommand;
|
|||||||
import click.kamil.springstatemachineexporter.service.ExportService;
|
import click.kamil.springstatemachineexporter.service.ExportService;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
// Enable diagnostic mode early before SLF4J initializes
|
||||||
|
for (String arg : args) {
|
||||||
|
if ("--debug".equals(arg)) {
|
||||||
|
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Manual DI / Wiring
|
// Manual DI / Wiring
|
||||||
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
|
||||||
var exportService = new ExportService(exporters);
|
var exportService = new ExportService(exporters);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
|
public interface AnalysisEnricher {
|
||||||
|
void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class CallChainEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
|
log.info("Enriching {} with call chains", result.getName());
|
||||||
|
|
||||||
|
List<CallChain> chains = intelligence.findCallChains(
|
||||||
|
result.getMetadata().getEntryPoints(),
|
||||||
|
result.getMetadata().getTriggers()
|
||||||
|
);
|
||||||
|
|
||||||
|
result.addMetadata(CodebaseMetadata.builder()
|
||||||
|
.callChains(chains)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class EntryPointEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
|
log.info("Enriching {} with entry points", result.getName());
|
||||||
|
|
||||||
|
List<EntryPoint> entryPoints = intelligence.findEntryPoints();
|
||||||
|
|
||||||
|
result.addMetadata(CodebaseMetadata.builder()
|
||||||
|
.entryPoints(entryPoints)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class PropertyEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
|
log.info("Enriching {} with properties", result.getName());
|
||||||
|
|
||||||
|
Map<String, Map<String, String>> properties = intelligence.resolveProperties();
|
||||||
|
|
||||||
|
result.addMetadata(CodebaseMetadata.builder()
|
||||||
|
.properties(properties)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
|
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
|
||||||
|
private final EventMatchingEngine matchingEngine = new HeuristicEventMatchingEngine();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
|
if (result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CallChain> updatedChains = new ArrayList<>();
|
||||||
|
List<Transition> stateMachineTransitions = result.getTransitions();
|
||||||
|
|
||||||
|
for (CallChain chain : result.getMetadata().getCallChains()) {
|
||||||
|
TriggerPoint tp = chain.getTriggerPoint();
|
||||||
|
if (tp == null || tp.getEvent() == null) {
|
||||||
|
updatedChains.add(chain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String triggerEvent = simplify(tp.getEvent());
|
||||||
|
String triggerSource = tp.getSourceState() != null ? simplify(tp.getSourceState()) : null;
|
||||||
|
|
||||||
|
List<MatchedTransition> matched = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Transition t : stateMachineTransitions) {
|
||||||
|
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
||||||
|
String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
|
||||||
|
// Event matches
|
||||||
|
for (State smSourceState : t.getSourceStates()) {
|
||||||
|
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
||||||
|
String smSource = simplify(smSourceRaw);
|
||||||
|
if (triggerSource == null || triggerSource.equals(smSource)) {
|
||||||
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
|
.sourceState(smSourceRaw)
|
||||||
|
.targetState(smSourceRaw)
|
||||||
|
.event(smEventRaw)
|
||||||
|
.build();
|
||||||
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)) {
|
||||||
|
matched.add(mt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (State smTargetState : t.getTargetStates()) {
|
||||||
|
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
||||||
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
|
.sourceState(smSourceRaw)
|
||||||
|
.targetState(targetRaw)
|
||||||
|
.event(smEventRaw)
|
||||||
|
.build();
|
||||||
|
if (isRoutedToCorrectMachine(chain, result.getName(), context)) {
|
||||||
|
matched.add(mt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matched.isEmpty()) {
|
||||||
|
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
|
||||||
|
updatedChains.add(newChain);
|
||||||
|
} else {
|
||||||
|
updatedChains.add(chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the metadata with the new call chains
|
||||||
|
CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
|
||||||
|
.triggers(result.getMetadata().getTriggers())
|
||||||
|
.entryPoints(result.getMetadata().getEntryPoints())
|
||||||
|
.callChains(updatedChains)
|
||||||
|
.properties(result.getMetadata().getProperties())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
result.setMetadata(updatedMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||||
|
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String simplify(String name) {
|
||||||
|
if (name == null) return null;
|
||||||
|
// Strip common suffixes
|
||||||
|
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||||
|
if (simplified.isEmpty()) {
|
||||||
|
simplified = name;
|
||||||
|
}
|
||||||
|
// Simplify full identifiers to just the last part (enum name)
|
||||||
|
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
|
||||||
|
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||||
|
// 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 shorter = smEvent.length() < triggerEvent.length() ? smEvent : triggerEvent;
|
||||||
|
String longer = smEvent.length() >= triggerEvent.length() ? smEvent : triggerEvent;
|
||||||
|
|
||||||
|
shorter = shorter.toLowerCase();
|
||||||
|
longer = longer.toLowerCase();
|
||||||
|
|
||||||
|
if (longer.contains(shorter)) {
|
||||||
|
// Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators
|
||||||
|
// to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE")
|
||||||
|
if (shorter.length() < 4) {
|
||||||
|
return longer.matches(".*(^|_|-)" + shorter + "(_|-|$).*");
|
||||||
|
}
|
||||||
|
// For longer strings, a straight contains is usually safe enough
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
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.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class TriggerEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
|
log.info("Enriching {} with triggers", result.getName());
|
||||||
|
|
||||||
|
List<TriggerPoint> triggers = intelligence.findTriggerPoints();
|
||||||
|
|
||||||
|
// Initially, we just add all triggers found in the codebase to the metadata.
|
||||||
|
// In later steps of Phase 2, we will filter them by State Machine ID.
|
||||||
|
result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||||
|
.triggers(triggers)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
|
|
||||||
|
public interface EventMatchingEngine {
|
||||||
|
/**
|
||||||
|
* Determines whether the given trigger point is a match for the state machine event.
|
||||||
|
*/
|
||||||
|
boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint);
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
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 HeuristicEventMatchingEngine implements EventMatchingEngine {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) {
|
||||||
|
if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String rawTriggerEvent = triggerPoint.getEvent();
|
||||||
|
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
||||||
|
String smEvent = simplify(smEventRaw);
|
||||||
|
|
||||||
|
boolean isWildcard = isWildcardVariable(rawTriggerEvent);
|
||||||
|
|
||||||
|
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
||||||
|
|
||||||
|
boolean hasPolyMatch = false;
|
||||||
|
for (String pe : polyEvents) {
|
||||||
|
if (pe.contains(".") && smEventRaw.contains(".")) {
|
||||||
|
if (smEventRaw.equals(pe) || smEventRaw.endsWith("." + pe) || pe.endsWith("." + smEventRaw)) {
|
||||||
|
hasPolyMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
if (pe.contains(".")) {
|
||||||
|
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
String simplifiedPe = simplify(simplePe);
|
||||||
|
|
||||||
|
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
||||||
|
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent) ||
|
||||||
|
isGuardedContains(smEvent, simplifiedPe)) {
|
||||||
|
hasPolyMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPolyMatch) return true;
|
||||||
|
if (polyEvents.isEmpty() && isWildcard) return true;
|
||||||
|
|
||||||
|
if (rawTriggerEvent.contains(".") && smEventRaw.contains(".")) {
|
||||||
|
if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent) || rawTriggerEvent.endsWith("." + smEventRaw)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String simplify(String name) {
|
||||||
|
if (name == null) return null;
|
||||||
|
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||||
|
if (simplified.isEmpty()) {
|
||||||
|
simplified = name;
|
||||||
|
}
|
||||||
|
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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, CodebaseContext context);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
String simplifiedMachineName = currentMachineName != null ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() : "";
|
||||||
|
|
||||||
|
if (targetVar != null && !targetVar.isEmpty()) {
|
||||||
|
targetVar = targetVar.toLowerCase();
|
||||||
|
if (targetVar.endsWith("statemachine")) {
|
||||||
|
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
|
||||||
|
if (!prefix.isEmpty()) {
|
||||||
|
if (simplifiedMachineName.contains(prefix)) {
|
||||||
|
return true; // Explicit positive match
|
||||||
|
} else {
|
||||||
|
return false; // Explicit negative match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (simplifiedMachineName.contains(targetVar)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) {
|
||||||
|
String smPackage = getPackageName(currentMachineName);
|
||||||
|
String smSimple = getSimpleClassName(currentMachineName);
|
||||||
|
String smPrefix = getFirstCamelCaseWord(smSimple);
|
||||||
|
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix, smSimple, chainSimple)) {
|
||||||
|
hasStrongMismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPositiveMatch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasStrongMismatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
||||||
|
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
||||||
|
|
||||||
|
if (smPackage.startsWith(chainPackage) || chainPackage.startsWith(smPackage)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find deepest common package root
|
||||||
|
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 the state machine's prefix is part of the shared common package,
|
||||||
|
// then the state machine represents the parent domain of the caller.
|
||||||
|
if (smPrefix != null && matchIdx >= 0) {
|
||||||
|
String lowerPrefix = smPrefix.toLowerCase();
|
||||||
|
for (int i = 0; i <= matchIdx; i++) {
|
||||||
|
if (p1[i].toLowerCase().contains(lowerPrefix) || lowerPrefix.contains(p1[i].toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 smLower = smPrefix.toLowerCase();
|
||||||
|
String chainLower = chainPrefix.toLowerCase();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onlyGenericDivergence) {
|
||||||
|
return true; // Mismatch under a shared root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> extractDomainTerms(String pkg, String prefix) {
|
||||||
|
Set<String> terms = new HashSet<>();
|
||||||
|
if (pkg != null && !pkg.isEmpty()) {
|
||||||
|
String[] segments = pkg.split("\\.");
|
||||||
|
// Take segments from index 2 onwards if length > 2
|
||||||
|
int start = segments.length > 2 ? 2 : 0;
|
||||||
|
for (int i = start; i < segments.length; i++) {
|
||||||
|
terms.add(segments[i].toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prefix != null && !prefix.isEmpty()) {
|
||||||
|
terms.add(prefix.toLowerCase());
|
||||||
|
}
|
||||||
|
return terms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPackageName(String fqn) {
|
||||||
|
if (fqn == null || !fqn.contains(".")) return "";
|
||||||
|
return fqn.substring(0, fqn.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSimpleClassName(String fqn) {
|
||||||
|
if (fqn == null) return "";
|
||||||
|
if (!fqn.contains(".")) return fqn;
|
||||||
|
return fqn.substring(fqn.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClassNameOnly(String methodFqn) {
|
||||||
|
if (methodFqn == null) return "";
|
||||||
|
String clean = methodFqn;
|
||||||
|
if (clean.contains("(")) {
|
||||||
|
clean = clean.substring(0, clean.indexOf('('));
|
||||||
|
}
|
||||||
|
if (clean.contains(".")) {
|
||||||
|
clean = clean.substring(0, clean.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFirstCamelCaseWord(String simpleName) {
|
||||||
|
if (simpleName == null || simpleName.isEmpty()) return null;
|
||||||
|
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
|
||||||
|
if (words.length > 0) {
|
||||||
|
return words[0];
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class AnalysisResult {
|
||||||
|
private String name;
|
||||||
|
@Builder.Default
|
||||||
|
private Set<click.kamil.springstatemachineexporter.model.State> states = java.util.Collections.emptySet();
|
||||||
|
private List<Transition> transitions;
|
||||||
|
private Set<String> startStates;
|
||||||
|
private Set<String> endStates;
|
||||||
|
private boolean renderChoicesAsDiamonds;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private List<BusinessFlow> flows = java.util.Collections.emptyList();
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
||||||
|
|
||||||
|
public void applyResolution(Map<String, String> properties) {
|
||||||
|
var resolver = new click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver();
|
||||||
|
|
||||||
|
// 1. Resolve start/end states strings
|
||||||
|
if (startStates != null) {
|
||||||
|
this.startStates = startStates.stream()
|
||||||
|
.map(s -> resolver.resolveValue(s, properties))
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
}
|
||||||
|
if (endStates != null) {
|
||||||
|
this.endStates = endStates.stream()
|
||||||
|
.map(s -> resolver.resolveValue(s, properties))
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Resolve states (State record is immutable, so recreate)
|
||||||
|
if (states != null) {
|
||||||
|
this.states = states.stream()
|
||||||
|
.map(s -> click.kamil.springstatemachineexporter.model.State.of(s.rawName(), resolver.resolveValue(s.fullIdentifier(), properties)))
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Resolve transitions
|
||||||
|
if (transitions != null) {
|
||||||
|
for (var t : transitions) {
|
||||||
|
if (t.getEvent() != null) {
|
||||||
|
t.setEvent(click.kamil.springstatemachineexporter.model.Event.of(t.getEvent().rawName(), resolver.resolveValue(t.getEvent().fullIdentifier(), properties)));
|
||||||
|
}
|
||||||
|
// Resolve states within transitions
|
||||||
|
t.setSourceStates(t.getSourceStates().stream()
|
||||||
|
.map(s -> click.kamil.springstatemachineexporter.model.State.of(s.rawName(), resolver.resolveValue(s.fullIdentifier(), properties)))
|
||||||
|
.collect(java.util.stream.Collectors.toList()));
|
||||||
|
t.setTargetStates(t.getTargetStates().stream()
|
||||||
|
.map(s -> click.kamil.springstatemachineexporter.model.State.of(s.rawName(), resolver.resolveValue(s.fullIdentifier(), properties)))
|
||||||
|
.collect(java.util.stream.Collectors.toList()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Resolve metadata (triggers, entry points)
|
||||||
|
if (metadata != null) {
|
||||||
|
if (metadata.getTriggers() != null) {
|
||||||
|
for (var trigger : metadata.getTriggers()) {
|
||||||
|
if (trigger.getEvent() != null) {
|
||||||
|
trigger.setEvent(resolver.resolveValue(trigger.getEvent(), properties));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (metadata.getEntryPoints() != null) {
|
||||||
|
for (var ep : metadata.getEntryPoints()) {
|
||||||
|
if (ep.getName() != null) {
|
||||||
|
ep.setName(resolver.resolveValue(ep.getName(), properties));
|
||||||
|
}
|
||||||
|
if (ep.getMetadata() != null) {
|
||||||
|
Map<String, String> resolvedMeta = new java.util.HashMap<>();
|
||||||
|
for (var entry : ep.getMetadata().entrySet()) {
|
||||||
|
resolvedMeta.put(entry.getKey(), resolver.resolveValue(entry.getValue(), properties));
|
||||||
|
}
|
||||||
|
ep.setMetadata(resolvedMeta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addMetadata(CodebaseMetadata newMetadata) {
|
||||||
|
if (newMetadata == null) return;
|
||||||
|
|
||||||
|
List<TriggerPoint> combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers());
|
||||||
|
if (newMetadata.getTriggers() != null) combinedTriggers.addAll(newMetadata.getTriggers());
|
||||||
|
|
||||||
|
List<EntryPoint> combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints());
|
||||||
|
if (newMetadata.getEntryPoints() != null) combinedEntryPoints.addAll(newMetadata.getEntryPoints());
|
||||||
|
|
||||||
|
List<CallChain> combinedCallChains = new java.util.ArrayList<>(this.metadata.getCallChains());
|
||||||
|
if (newMetadata.getCallChains() != null) combinedCallChains.addAll(newMetadata.getCallChains());
|
||||||
|
|
||||||
|
java.util.Map<String, java.util.Map<String, String>> combinedProperties = new java.util.HashMap<>(this.metadata.getProperties());
|
||||||
|
if (newMetadata.getProperties() != null) combinedProperties.putAll(newMetadata.getProperties());
|
||||||
|
|
||||||
|
this.metadata = CodebaseMetadata.builder()
|
||||||
|
.triggers(Collections.unmodifiableList(combinedTriggers))
|
||||||
|
.entryPoints(Collections.unmodifiableList(combinedEntryPoints))
|
||||||
|
.callChains(Collections.unmodifiableList(combinedCallChains))
|
||||||
|
.properties(Collections.unmodifiableMap(combinedProperties))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class BusinessFlow {
|
||||||
|
private final String name;
|
||||||
|
private final String description;
|
||||||
|
private final List<String> steps; // List of Event names in order
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class CallChain {
|
||||||
|
private final EntryPoint entryPoint;
|
||||||
|
private final List<String> methodChain; // e.g., ["Controller.submit", "Service.process", "Service.send"]
|
||||||
|
private final TriggerPoint triggerPoint;
|
||||||
|
private final String contextMachineId;
|
||||||
|
private final List<MatchedTransition> matchedTransitions;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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 String targetMethod;
|
||||||
|
private List<String> arguments;
|
||||||
|
private String receiver;
|
||||||
|
|
||||||
|
public CallEdge(String targetMethod, List<String> arguments) {
|
||||||
|
this(targetMethod, arguments, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class CodebaseMetadata {
|
||||||
|
@Builder.Default
|
||||||
|
private final List<TriggerPoint> triggers = Collections.emptyList();
|
||||||
|
@Builder.Default
|
||||||
|
private final List<EntryPoint> entryPoints = Collections.emptyList();
|
||||||
|
@Builder.Default
|
||||||
|
private final List<CallChain> callChains = Collections.emptyList();
|
||||||
|
@Builder.Default
|
||||||
|
private final Map<String, Map<String, String>> properties = Collections.emptyMap();
|
||||||
|
|
||||||
|
public static CodebaseMetadata empty() {
|
||||||
|
return CodebaseMetadata.builder().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class EntryPoint {
|
||||||
|
public enum Type { REST, KAFKA, RABBIT, JMS, SQS, SNS, CUSTOM }
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class Parameter {
|
||||||
|
private final String name;
|
||||||
|
private final String type;
|
||||||
|
private final List<String> annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Type type;
|
||||||
|
private String name; // e.g., "POST /orders" or "Kafka Topic: orders"
|
||||||
|
private final String className;
|
||||||
|
private final String methodName;
|
||||||
|
private final String sourceFile;
|
||||||
|
private Map<String, String> metadata; // e.g., path, topic, exchange
|
||||||
|
@Builder.Default
|
||||||
|
private final List<Parameter> parameters = java.util.Collections.emptyList();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class LibraryHint {
|
||||||
|
private final String methodFqn; // e.g., "com.thirdparty.Workflow.send"
|
||||||
|
private final String event; // The event it triggers (static)
|
||||||
|
private final Integer eventArgumentIndex; // e.g., 0 to extract from the 0th argument
|
||||||
|
private final String eventArgumentMethod; // e.g., "getType" to extract from argument method call
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class MatchedTransition {
|
||||||
|
private final String sourceState;
|
||||||
|
private final String targetState;
|
||||||
|
private final String event;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
@Jacksonized
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class TriggerPoint {
|
||||||
|
private String event;
|
||||||
|
private final String className;
|
||||||
|
private final String methodName;
|
||||||
|
private final String sourceFile;
|
||||||
|
private final String sourceModule;
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,651 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ConstantResolver {
|
||||||
|
|
||||||
|
public String resolve(Expression expr, CodebaseContext context) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Unwrap common wrappers
|
||||||
|
if (expr instanceof CastExpression ce) {
|
||||||
|
return resolveInternal(ce.getExpression(), context, visited);
|
||||||
|
}
|
||||||
|
if (expr instanceof ParenthesizedExpression pe) {
|
||||||
|
return resolveInternal(pe.getExpression(), context, visited);
|
||||||
|
}
|
||||||
|
if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
|
||||||
|
if (!mi.arguments().isEmpty()) {
|
||||||
|
return resolveInternal((Expression) mi.arguments().get(0), context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Literal?
|
||||||
|
if (expr instanceof StringLiteral sl) {
|
||||||
|
return sl.getLiteralValue();
|
||||||
|
}
|
||||||
|
if (expr instanceof NumberLiteral nl) {
|
||||||
|
return nl.getToken();
|
||||||
|
}
|
||||||
|
if (expr instanceof BooleanLiteral bl) {
|
||||||
|
return String.valueOf(bl.booleanValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. JDT Bindings? (Most robust)
|
||||||
|
IVariableBinding binding = null;
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
IBinding b = sn.resolveBinding();
|
||||||
|
if (b instanceof IVariableBinding vb) binding = vb;
|
||||||
|
} else if (expr instanceof QualifiedName qn) {
|
||||||
|
IBinding b = qn.resolveBinding();
|
||||||
|
if (b instanceof IVariableBinding vb) binding = vb;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding != null && binding.isField()) {
|
||||||
|
Object value = binding.getConstantValue();
|
||||||
|
if (value != null) return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback: Manual AST Traversal (if bindings not available or resolved)
|
||||||
|
if (expr instanceof InfixExpression infix) {
|
||||||
|
return resolveInfix(infix, context, visited);
|
||||||
|
}
|
||||||
|
if (expr instanceof QualifiedName qn) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
if ((mi.getName().getIdentifier().equals("name") || mi.getName().getIdentifier().equals("toString")) && mi.arguments().isEmpty()) {
|
||||||
|
String val = resolveInternal(mi.getExpression(), context, visited);
|
||||||
|
if (val != null) {
|
||||||
|
// If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it
|
||||||
|
if (val.contains(".")) {
|
||||||
|
return val.substring(val.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for unresolved AST nodes
|
||||||
|
if (mi.getExpression() instanceof QualifiedName qn) {
|
||||||
|
System.out.println("RETURNING qn name: " + qn.getName().getIdentifier()); return qn.getName().getIdentifier();
|
||||||
|
} else if (mi.getExpression() instanceof SimpleName sn) {
|
||||||
|
System.out.println("RETURNING sn name: " + sn.getIdentifier()); return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("valueOf".equals(mi.getName().getIdentifier()) && mi.getExpression() != null) {
|
||||||
|
java.util.List<String> enumValues = context.getEnumValues(mi.getExpression().toString());
|
||||||
|
if (enumValues != null && !enumValues.isEmpty()) {
|
||||||
|
return "ENUM_SET:" + String.join(",", enumValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMethodBinding mb = mi.resolveMethodBinding();
|
||||||
|
if (mb != null) {
|
||||||
|
ITypeBinding returnType = mb.getReturnType();
|
||||||
|
if (returnType != null) {
|
||||||
|
java.util.List<String> values = context.getEnumValues(returnType.getQualifiedName());
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
return "ENUM_SET:" + String.join(",", values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
String left = resolveInternal(expr.getLeftOperand(), context, visited);
|
||||||
|
String right = resolveInternal(expr.getRightOperand(), context, visited);
|
||||||
|
|
||||||
|
if (left == null || right == null) return null;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(left + right);
|
||||||
|
if (expr.hasExtendedOperands()) {
|
||||||
|
for (Object operand : expr.extendedOperands()) {
|
||||||
|
String val = resolveInternal((Expression) operand, context, visited);
|
||||||
|
if (val == null) return null;
|
||||||
|
sb.append(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
String result = resolveFieldInType(td, sn.getIdentifier(), fqn, context, visited);
|
||||||
|
if (result != null) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Static imports
|
||||||
|
ASTNode root = sn.getRoot();
|
||||||
|
if (root instanceof CompilationUnit cu) {
|
||||||
|
TypeDeclaration staticTd = context.resolveStaticImport(sn.getIdentifier(), cu);
|
||||||
|
if (staticTd != null) {
|
||||||
|
return resolveFieldInType(staticTd, sn.getIdentifier(), context.getFqn(staticTd), context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveManual(QualifiedName qn, CodebaseContext context, Set<String> visited) {
|
||||||
|
String qualifier = qn.getQualifier().toString();
|
||||||
|
String name = qn.getName().getIdentifier();
|
||||||
|
|
||||||
|
CompilationUnit contextCu = (CompilationUnit) qn.getRoot();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(qualifier, contextCu);
|
||||||
|
|
||||||
|
if (td != null) {
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
return resolveFieldInType(td, name, fqn, context, visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveFieldInType(TypeDeclaration td, String fieldName, String typeFqn, CodebaseContext context, Set<String> visited) {
|
||||||
|
String fieldId = typeFqn + "#" + fieldName;
|
||||||
|
if (visited.contains(fieldId)) {
|
||||||
|
log.warn("Circular reference detected during constant resolution: {}", fieldId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
visited.add(fieldId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragmentObj : fd.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(fieldName)) {
|
||||||
|
// Check for @Value annotation first
|
||||||
|
String valueFromAnnotation = findValueFromAnnotation(fd);
|
||||||
|
if (valueFromAnnotation != null) return valueFromAnnotation;
|
||||||
|
|
||||||
|
Expression initializer = fragment.getInitializer();
|
||||||
|
if (initializer != null) {
|
||||||
|
return resolveInternal(initializer, context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findValueFromAnnotation(FieldDeclaration fd) {
|
||||||
|
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) {
|
||||||
|
Expression valueExpr = null;
|
||||||
|
if (ann instanceof SingleMemberAnnotation sma) {
|
||||||
|
valueExpr = sma.getValue();
|
||||||
|
} else if (ann instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if ("value".equals(pair.getName().getIdentifier())) {
|
||||||
|
valueExpr = pair.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueExpr instanceof StringLiteral sl) {
|
||||||
|
return sl.getLiteralValue();
|
||||||
|
}
|
||||||
|
return valueExpr != null ? valueExpr.toString().replaceAll("^\"|\"$", "") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PolymorphicEventResolver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to resolve a dynamically passed event variable into a set of
|
||||||
|
* concrete polymorphic event names.
|
||||||
|
*
|
||||||
|
* @param triggerPoint The original trigger point to resolve.
|
||||||
|
* @param resolvedValue The current String expression (e.g., "event.getType()" or "event")
|
||||||
|
* @param path The call path trace leading up to this variable.
|
||||||
|
* @param context The codebase context for deep type resolution.
|
||||||
|
* @return The updated TriggerPoint with polymorphic events set, or the original if unable to resolve.
|
||||||
|
*/
|
||||||
|
TriggerPoint resolvePolymorphicEvents(TriggerPoint triggerPoint, String resolvedValue, List<String> path, CodebaseContext context);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class PropertyResolver {
|
||||||
|
|
||||||
|
private final List<String> activeProfiles = new ArrayList<>();
|
||||||
|
|
||||||
|
public void setActiveProfiles(List<String> profiles) {
|
||||||
|
this.activeProfiles.clear();
|
||||||
|
if (profiles != null) {
|
||||||
|
this.activeProfiles.addAll(profiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> resolveProperties(Path rootDir) {
|
||||||
|
Map<String, String> allProperties = new HashMap<>();
|
||||||
|
|
||||||
|
// 1. Load base properties
|
||||||
|
loadMatching(rootDir, "application.properties", allProperties);
|
||||||
|
loadMatching(rootDir, "application.yml", allProperties);
|
||||||
|
loadMatching(rootDir, "application.yaml", allProperties);
|
||||||
|
|
||||||
|
// 2. Load profile-specific properties (overriding base)
|
||||||
|
for (String profile : activeProfiles) {
|
||||||
|
loadMatching(rootDir, "application-" + profile + ".properties", allProperties);
|
||||||
|
loadMatching(rootDir, "application-" + profile + ".yml", allProperties);
|
||||||
|
loadMatching(rootDir, "application-" + profile + ".yaml", allProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Map<String, String>> resolveAllProperties(Set<Path> rootDirs) {
|
||||||
|
Map<String, Map<String, String>> profiles = new HashMap<>();
|
||||||
|
|
||||||
|
for (Path rootDir : rootDirs) {
|
||||||
|
// 1. Load default profile
|
||||||
|
Map<String, String> defaultProps = profiles.computeIfAbsent("default", k -> new HashMap<>());
|
||||||
|
loadMatching(rootDir, "application.properties", defaultProps);
|
||||||
|
loadMatching(rootDir, "application.yml", defaultProps);
|
||||||
|
loadMatching(rootDir, "application.yaml", defaultProps);
|
||||||
|
|
||||||
|
// 2. Discover and load all other profiles
|
||||||
|
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||||
|
paths.filter(Files::isRegularFile)
|
||||||
|
.filter(p -> {
|
||||||
|
String name = p.getFileName().toString();
|
||||||
|
return name.startsWith("application-") &&
|
||||||
|
(name.endsWith(".properties") || name.endsWith(".yml") || name.endsWith(".yaml"));
|
||||||
|
})
|
||||||
|
.forEach(p -> {
|
||||||
|
String name = p.getFileName().toString();
|
||||||
|
String profile = name.substring("application-".length(), name.lastIndexOf('.'));
|
||||||
|
Map<String, String> loaded = p.toString().endsWith(".properties") ? loadProperties(p) : loadYaml(p);
|
||||||
|
profiles.computeIfAbsent(profile, k -> new HashMap<>()).putAll(loaded);
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to scan for profiles in {}", rootDir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadMatching(Path rootDir, String fileName, Map<String, String> target) {
|
||||||
|
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||||
|
paths.filter(p -> Files.isRegularFile(p))
|
||||||
|
.filter(p -> p.getFileName().toString().equals(fileName))
|
||||||
|
.forEach(p -> {
|
||||||
|
if (p.toString().endsWith(".properties")) {
|
||||||
|
target.putAll(loadProperties(p));
|
||||||
|
} else {
|
||||||
|
target.putAll(loadYaml(p));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to scan for {} in {}", fileName, rootDir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> loadProperties(Path path) {
|
||||||
|
Map<String, String> props = new HashMap<>();
|
||||||
|
Properties p = new Properties();
|
||||||
|
try (InputStream is = Files.newInputStream(path)) {
|
||||||
|
p.load(is);
|
||||||
|
p.forEach((k, v) -> props.put(k.toString(), v.toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to load properties from {}", path);
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> loadYaml(Path path) {
|
||||||
|
Map<String, String> props = new HashMap<>();
|
||||||
|
try {
|
||||||
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(new com.fasterxml.jackson.dataformat.yaml.YAMLFactory());
|
||||||
|
Map<String, Object> map = mapper.readValue(path.toFile(), new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
|
||||||
|
flattenYaml("", map, props);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to load YAML from {}", path);
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void flattenYaml(String prefix, Map<String, Object> map, Map<String, String> result) {
|
||||||
|
if (map == null) return;
|
||||||
|
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||||
|
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof Map) {
|
||||||
|
flattenYaml(key, (Map<String, Object>) value, result);
|
||||||
|
} else if (value != null) {
|
||||||
|
result.put(key, value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveValue(String placeholder, Map<String, String> properties) {
|
||||||
|
return click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver.resolve(placeholder, properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class SiblingDependencyResolver {
|
||||||
|
|
||||||
|
private static final Pattern GRADLE_PROJECT_DEP = Pattern.compile("project\\(['\"]:(.*?)['\"]\\)");
|
||||||
|
private static final Pattern MAVEN_DEPENDENCY = Pattern.compile("<dependency>\\s*<groupId>(.*?)</groupId>\\s*<artifactId>(.*?)</artifactId>\\s*<version>(.*?)</version>", Pattern.DOTALL);
|
||||||
|
|
||||||
|
public Set<Path> resolveSiblings(Path projectDir) {
|
||||||
|
Set<Path> siblings = new HashSet<>();
|
||||||
|
try {
|
||||||
|
siblings.addAll(resolveGradleSiblings(projectDir));
|
||||||
|
siblings.addAll(resolveMavenSiblings(projectDir));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error resolving sibling dependencies for {}", projectDir, e);
|
||||||
|
}
|
||||||
|
return siblings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Path> resolveGradleSiblings(Path projectDir) throws IOException {
|
||||||
|
Set<Path> siblings = new HashSet<>();
|
||||||
|
Path buildGradle = projectDir.resolve("build.gradle");
|
||||||
|
if (!Files.exists(buildGradle)) {
|
||||||
|
buildGradle = projectDir.resolve("build.gradle.kts");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(buildGradle)) {
|
||||||
|
log.info("Found build.gradle at {}", buildGradle);
|
||||||
|
String content = Files.readString(buildGradle);
|
||||||
|
Matcher matcher = GRADLE_PROJECT_DEP.matcher(content);
|
||||||
|
Set<String> gradlePaths = new HashSet<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
gradlePaths.add(matcher.group(1));
|
||||||
|
}
|
||||||
|
log.info("Found project dependencies: {}", gradlePaths);
|
||||||
|
|
||||||
|
if (!gradlePaths.isEmpty()) {
|
||||||
|
Path rootDir = findGradleRoot(projectDir);
|
||||||
|
log.info("Resolved Gradle root to: {}", rootDir);
|
||||||
|
if (rootDir != null) {
|
||||||
|
for (String gradlePath : gradlePaths) {
|
||||||
|
String relativePath = gradlePath.replace(':', '/');
|
||||||
|
Path resolvedSibling = rootDir.resolve(relativePath).normalize();
|
||||||
|
if (Files.exists(resolvedSibling)) {
|
||||||
|
log.info("Discovered local Gradle sibling dependency: {}", resolvedSibling);
|
||||||
|
siblings.add(resolvedSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return siblings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path findGradleRoot(Path start) {
|
||||||
|
Path current = start.toAbsolutePath();
|
||||||
|
Path lastSettingsDir = null;
|
||||||
|
while (current != null) {
|
||||||
|
if (Files.exists(current.resolve("settings.gradle")) || Files.exists(current.resolve("settings.gradle.kts"))) {
|
||||||
|
lastSettingsDir = current;
|
||||||
|
}
|
||||||
|
if (Files.exists(current.resolve(".git"))) {
|
||||||
|
return lastSettingsDir != null ? lastSettingsDir : current;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return lastSettingsDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Path> resolveMavenSiblings(Path projectDir) throws IOException {
|
||||||
|
Set<Path> siblings = new HashSet<>();
|
||||||
|
Path pom = projectDir.resolve("pom.xml");
|
||||||
|
if (Files.exists(pom)) {
|
||||||
|
log.info("Found pom.xml at {}", pom);
|
||||||
|
String content = Files.readString(pom);
|
||||||
|
|
||||||
|
Set<String> targetArtifacts = new HashSet<>();
|
||||||
|
Matcher matcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(content);
|
||||||
|
// We want artifacts from the <dependencies> section
|
||||||
|
int depsStart = content.indexOf("<dependencies>");
|
||||||
|
int depsEnd = content.indexOf("</dependencies>");
|
||||||
|
if (depsStart != -1 && depsEnd > depsStart) {
|
||||||
|
String depsContent = content.substring(depsStart, depsEnd);
|
||||||
|
Matcher depMatcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(depsContent);
|
||||||
|
while (depMatcher.find()) {
|
||||||
|
targetArtifacts.add(depMatcher.group(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("Found maven target artifacts: {}", targetArtifacts);
|
||||||
|
|
||||||
|
if (!targetArtifacts.isEmpty()) {
|
||||||
|
Path rootDir = findMavenRoot(projectDir);
|
||||||
|
log.info("Resolved Maven root to: {}", rootDir);
|
||||||
|
if (rootDir != null) {
|
||||||
|
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||||
|
List<Path> allPoms = paths.filter(p -> p.getFileName().toString().equals("pom.xml"))
|
||||||
|
.filter(p -> !p.toString().contains("/target/"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.info("Scanned {} total pom.xml files under {}", allPoms.size(), rootDir);
|
||||||
|
|
||||||
|
for (Path p : allPoms) {
|
||||||
|
try {
|
||||||
|
String pContent = Files.readString(p);
|
||||||
|
String artifactId = extractOwnArtifactId(pContent);
|
||||||
|
log.info("Checking artifactId {} in {}", artifactId, p);
|
||||||
|
if (artifactId != null && targetArtifacts.contains(artifactId)) {
|
||||||
|
Path siblingDir = p.getParent();
|
||||||
|
if (!siblingDir.equals(projectDir)) {
|
||||||
|
log.info("Discovered local Maven sibling dependency: {}", siblingDir);
|
||||||
|
siblings.add(siblingDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return siblings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractOwnArtifactId(String pomContent) {
|
||||||
|
// Strip <parent> block if it exists
|
||||||
|
String content = pomContent;
|
||||||
|
int parentEnd = pomContent.indexOf("</parent>");
|
||||||
|
if (parentEnd != -1) {
|
||||||
|
content = pomContent.substring(parentEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher matcher = Pattern.compile("<artifactId>(.*?)</artifactId>").matcher(content);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path findMavenRoot(Path start) {
|
||||||
|
Path current = start.toAbsolutePath();
|
||||||
|
Path lastPomDir = null;
|
||||||
|
while (current != null) {
|
||||||
|
if (Files.exists(current.resolve("pom.xml"))) {
|
||||||
|
lastPomDir = current;
|
||||||
|
}
|
||||||
|
if (Files.exists(current.resolve(".git"))) {
|
||||||
|
return lastPomDir != null ? lastPomDir : current;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return lastPomDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
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 java.util.List;
|
||||||
|
|
||||||
|
public interface CallGraphEngine {
|
||||||
|
List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
|
||||||
|
}
|
||||||
@@ -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,30 @@
|
|||||||
|
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 java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface CodebaseIntelligenceProvider {
|
||||||
|
/**
|
||||||
|
* Discovers all locations where events are triggered.
|
||||||
|
*/
|
||||||
|
List<TriggerPoint> findTriggerPoints();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers all entry points (REST, Kafka, etc.).
|
||||||
|
*/
|
||||||
|
List<EntryPoint> findEntryPoints();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Links Entry Points to Trigger Points.
|
||||||
|
*/
|
||||||
|
List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves configuration properties per profile.
|
||||||
|
*/
|
||||||
|
Map<String, Map<String, String>> resolveProperties();
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ConstantExtractor {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
private final Function<MethodInvocation, String> calledMethodResolver;
|
||||||
|
private VariableTracer variableTracer;
|
||||||
|
private ConstructorAnalyzer constructorAnalyzer;
|
||||||
|
|
||||||
|
public ConstantExtractor(CodebaseContext context, ConstantResolver constantResolver, Function<MethodInvocation, String> calledMethodResolver) {
|
||||||
|
this.context = context;
|
||||||
|
this.constantResolver = constantResolver;
|
||||||
|
this.calledMethodResolver = calledMethodResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVariableTracer(VariableTracer variableTracer) {
|
||||||
|
this.variableTracer = variableTracer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstructorAnalyzer(ConstructorAnalyzer constructorAnalyzer) {
|
||||||
|
this.constructorAnalyzer = constructorAnalyzer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
|
||||||
|
if (expr instanceof QualifiedName qn) {
|
||||||
|
constants.add(qn.toString());
|
||||||
|
} else if (expr instanceof StringLiteral sl) {
|
||||||
|
constants.add(sl.getLiteralValue());
|
||||||
|
} else if (expr instanceof ConditionalExpression ce) {
|
||||||
|
extractConstantsFromExpression(ce.getThenExpression(), constants);
|
||||||
|
extractConstantsFromExpression(ce.getElseExpression(), constants);
|
||||||
|
} else if (expr instanceof ParenthesizedExpression pe) {
|
||||||
|
extractConstantsFromExpression(pe.getExpression(), constants);
|
||||||
|
} else if (expr instanceof CastExpression ce) {
|
||||||
|
extractConstantsFromExpression(ce.getExpression(), constants);
|
||||||
|
} else if (expr instanceof Assignment assignment) {
|
||||||
|
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
||||||
|
} else if (expr instanceof SwitchExpression se) {
|
||||||
|
se.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(YieldStatement ys) {
|
||||||
|
extractConstantsFromExpression(ys.getExpression(), constants);
|
||||||
|
return super.visit(ys);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ExpressionStatement es) {
|
||||||
|
extractConstantsFromExpression(es.getExpression(), constants);
|
||||||
|
return super.visit(es);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement rs) {
|
||||||
|
extractConstantsFromExpression(rs.getExpression(), constants);
|
||||||
|
return super.visit(rs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (expr instanceof ArrayAccess aa) {
|
||||||
|
extractConstantsFromExpression(aa.getArray(), constants);
|
||||||
|
} else if (expr instanceof ArrayCreation ac && ac.getInitializer() != null) {
|
||||||
|
for (Object expObj : ac.getInitializer().expressions()) {
|
||||||
|
extractConstantsFromArgument((Expression) expObj, constants);
|
||||||
|
}
|
||||||
|
} else if (expr instanceof ArrayInitializer ai) {
|
||||||
|
for (Object expObj : ai.expressions()) {
|
||||||
|
extractConstantsFromArgument((Expression) expObj, constants);
|
||||||
|
}
|
||||||
|
} else if (expr instanceof MethodInvocation mi) {
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
|
||||||
|
System.out.println("Processing mi: " + mi); if (methodName.equals("get") || methodName.equals("getOrDefault")) {
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
if (variableTracer != null) {
|
||||||
|
List<Expression> traced = variableTracer.traceVariableAll(mi.getExpression());
|
||||||
|
for (Expression t : traced) {
|
||||||
|
extractConstantsFromExpression(t, constants);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extractConstantsFromExpression(mi.getExpression(), constants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry") ||
|
||||||
|
methodName.equals("just") || methodName.equals("withPayload") || methodName.equals("success")) {
|
||||||
|
for (Object argObj : mi.arguments()) {
|
||||||
|
extractConstantsFromExpression((Expression) argObj, constants);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof SimpleName sn) {
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
String propName = methodName.startsWith("get") ? methodName.substring(3) : methodName;
|
||||||
|
|
||||||
|
Block block = findEnclosingBlock(mi);
|
||||||
|
if (block != null) {
|
||||||
|
for (Object stmtObj : block.statements()) {
|
||||||
|
if (stmtObj == mi.getParent() || stmtObj == mi) break;
|
||||||
|
if (stmtObj instanceof ExpressionStatement es) {
|
||||||
|
if (es.getExpression() instanceof MethodInvocation setterMi) {
|
||||||
|
if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
if (setterMi.getExpression() instanceof SimpleName setterSn && setterSn.getIdentifier().equals(varName)) {
|
||||||
|
if (!setterMi.arguments().isEmpty()) {
|
||||||
|
extractConstantsFromExpression((Expression) setterMi.arguments().get(0), constants);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (es.getExpression() instanceof Assignment assignment) {
|
||||||
|
if (assignment.getLeftHandSide() instanceof FieldAccess fa) {
|
||||||
|
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
||||||
|
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (assignment.getLeftHandSide() instanceof QualifiedName qqn) {
|
||||||
|
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
||||||
|
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Expression> receivers = new ArrayList<>();
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
if (mi.getExpression() instanceof ClassInstanceCreation) {
|
||||||
|
receivers.add(mi.getExpression());
|
||||||
|
} else if (variableTracer != null) {
|
||||||
|
receivers.addAll(variableTracer.traceVariableAll(mi.getExpression()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Expression receiverExpr : receivers) {
|
||||||
|
if (receiverExpr instanceof ClassInstanceCreation cic) {
|
||||||
|
if (cic.getAnonymousClassDeclaration() != null) {
|
||||||
|
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||||
|
if (declObj instanceof MethodDeclaration mdecl) {
|
||||||
|
if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) {
|
||||||
|
mdecl.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement rs) {
|
||||||
|
if (rs.getExpression() != null) {
|
||||||
|
extractConstantsFromExpression(rs.getExpression(), constants);
|
||||||
|
}
|
||||||
|
return super.visit(rs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (constructorAnalyzer != null) {
|
||||||
|
boolean handledByLombok = false;
|
||||||
|
if (methodName.startsWith("get") && methodName.length() > 3) {
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
TypeDeclaration typeDecl = context.getTypeDeclaration(typeName);
|
||||||
|
if (typeDecl != null) {
|
||||||
|
String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
|
||||||
|
int fieldIndex = constructorAnalyzer.findConstructorArgumentIndexForLombokField(typeDecl, fieldName);
|
||||||
|
if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) {
|
||||||
|
extractConstantsFromArgument((Expression) cic.arguments().get(fieldIndex), constants);
|
||||||
|
handledByLombok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handledByLombok && methodName != null && !methodName.isEmpty()) {
|
||||||
|
List<String> narrowed = constructorAnalyzer.resolveInlineInstantiationGetterResult(
|
||||||
|
cic, methodName, context, new HashSet<>());
|
||||||
|
if (narrowed.isEmpty()) {
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
CompilationUnit contextCu = expr.getRoot() instanceof CompilationUnit ? (CompilationUnit) expr.getRoot() : null;
|
||||||
|
TypeDeclaration typeDecl = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName);
|
||||||
|
if (typeDecl == null) typeDecl = context.getTypeDeclaration(typeName);
|
||||||
|
if (typeDecl != null) {
|
||||||
|
narrowed = resolveMethodReturnConstant(context.getFqn(typeDecl), methodName, 0, new HashSet<>(), contextCu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (narrowed != null && !narrowed.isEmpty()) {
|
||||||
|
constants.addAll(narrowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression currentMi = mi;
|
||||||
|
while (currentMi instanceof MethodInvocation chainMi) {
|
||||||
|
String chainName = chainMi.getName().getIdentifier();
|
||||||
|
if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) {
|
||||||
|
extractConstantsFromExpression((Expression) chainMi.arguments().get(1), constants);
|
||||||
|
} else if ((chainName.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) {
|
||||||
|
extractConstantsFromExpression((Expression) chainMi.arguments().get(0), constants);
|
||||||
|
}
|
||||||
|
currentMi = chainMi.getExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration td = findEnclosingType(mi);
|
||||||
|
if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof ThisExpression)) {
|
||||||
|
List<String> values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new HashSet<>(), null);
|
||||||
|
if (values != null) constants.addAll(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void extractConstantsFromArgument(Expression argObj, List<String> constants) {
|
||||||
|
if (argObj instanceof ClassInstanceCreation innerCic) {
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
|
||||||
|
if ("String".equals(typeName)) {
|
||||||
|
extractConstantsFromExpression(argObj, constants);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extractConstantsFromExpression(argObj, constants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) {
|
||||||
|
if (className != null && className.contains(".")) {
|
||||||
|
String prefix = className.substring(0, className.lastIndexOf('.'));
|
||||||
|
String suffix = className.substring(className.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration prefixTd = contextCu != null ? context.getTypeDeclaration(prefix, contextCu) : context.getTypeDeclaration(prefix);
|
||||||
|
if (prefixTd == null) prefixTd = context.getTypeDeclaration(prefix);
|
||||||
|
boolean isKnownType = prefixTd != null || context.getEnumValues(prefix) != null;
|
||||||
|
if (isKnownType) {
|
||||||
|
String simplePrefix = prefix.contains(".") ? prefix.substring(prefix.lastIndexOf('.') + 1) : prefix;
|
||||||
|
return Collections.singletonList(simplePrefix + "." + suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
|
||||||
|
if (td == null) td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) return null;
|
||||||
|
|
||||||
|
final List<String> resolvedConstants = new ArrayList<>();
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
if (variableTracer != null) {
|
||||||
|
List<Expression> tracedReturns = variableTracer.traceVariableAll(node.getExpression());
|
||||||
|
for (Expression tracedRet : tracedReturns) {
|
||||||
|
extractConstantsFromExpression(tracedRet, resolvedConstants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!resolvedConstants.isEmpty()) {
|
||||||
|
return resolvedConstants;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
||||||
|
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
||||||
|
if (depth > 20) {
|
||||||
|
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String fqn = className + "." + methodName;
|
||||||
|
if (!visited.add(fqn)) {
|
||||||
|
log.debug("Exiting resolveMethodReturnConstant early (cycle detected) for fqn={}", fqn);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||||
|
if (tempTd == null) {
|
||||||
|
tempTd = context.getTypeDeclaration(className);
|
||||||
|
}
|
||||||
|
final TypeDeclaration td = tempTd;
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
Expression retExpr = node.getExpression();
|
||||||
|
if (retExpr != null) {
|
||||||
|
boolean handled = false;
|
||||||
|
if (retExpr instanceof MethodInvocation mi && calledMethodResolver != null) {
|
||||||
|
String called = calledMethodResolver.apply(mi);
|
||||||
|
if (called != null && called.contains(".")) {
|
||||||
|
if (visited.contains(called)) {
|
||||||
|
handled = true;
|
||||||
|
} else {
|
||||||
|
String cName = called.substring(0, called.lastIndexOf('.'));
|
||||||
|
String mName = called.substring(called.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
|
||||||
|
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||||
|
|
||||||
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
|
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||||
|
constants.addAll(delegationResult);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (retExpr instanceof SuperMethodInvocation smi) {
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null) {
|
||||||
|
String called = superFqn + "." + smi.getName().getIdentifier();
|
||||||
|
if (visited.contains(called)) {
|
||||||
|
handled = true;
|
||||||
|
} else {
|
||||||
|
String cName = superFqn;
|
||||||
|
String mName = smi.getName().getIdentifier();
|
||||||
|
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
|
||||||
|
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||||
|
|
||||||
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
|
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||||
|
constants.addAll(delegationResult);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled && variableTracer != null && constructorAnalyzer != null) {
|
||||||
|
List<Expression> tracedRetExprs = variableTracer.traceVariableAll(retExpr);
|
||||||
|
for (Expression tracedRetExpr : tracedRetExprs) {
|
||||||
|
extractConstantsFromExpression(tracedRetExpr, constants);
|
||||||
|
String val = constantResolver.resolve(tracedRetExpr, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
String parsed = parseEnumSetElement(eVal);
|
||||||
|
if (!constants.contains(parsed)) constants.add(parsed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!constants.contains(val)) constants.add(val);
|
||||||
|
}
|
||||||
|
} else if (tracedRetExpr instanceof SimpleName sn) {
|
||||||
|
List<String> consts = constructorAnalyzer.traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
constants.addAll(consts);
|
||||||
|
}
|
||||||
|
} else if (tracedRetExpr instanceof FieldAccess fa) {
|
||||||
|
List<String> consts = constructorAnalyzer.traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited);
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
constants.addAll(consts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
List<String> impls = context.getImplementations(className);
|
||||||
|
if (impls != null) {
|
||||||
|
for (String implName : impls) {
|
||||||
|
List<String> delegationResult = resolveMethodReturnConstant(implName, methodName, depth + 1, visited, contextCu);
|
||||||
|
constants.addAll(delegationResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited.remove(fqn);
|
||||||
|
log.debug("resolveMethodReturnConstant for class={}, method={} resolved constants: {}", className, methodName, constants);
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String parseEnumSetElement(String eVal) {
|
||||||
|
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Block findEnclosingBlock(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof Block)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (Block) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (TypeDeclaration) parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,808 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class ConstructorAnalyzer {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
private VariableTracer variableTracer;
|
||||||
|
private ConstantExtractor constantExtractor;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface RhsResolver {
|
||||||
|
String resolve(Expression rhs, Map<String, String> paramValues, TypeDeclaration td);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConstructorAnalyzer(CodebaseContext context, ConstantResolver constantResolver) {
|
||||||
|
this.context = context;
|
||||||
|
this.constantResolver = constantResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVariableTracer(VariableTracer variableTracer) {
|
||||||
|
this.variableTracer = variableTracer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstantExtractor(ConstantExtractor constantExtractor) {
|
||||||
|
this.constantExtractor = constantExtractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
|
if (td == null || fieldName == null) return Collections.emptyList();
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (fqn == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
String cycleKey = fqn + "#" + fieldName;
|
||||||
|
if (!visited.add(cycleKey)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
|
||||||
|
Expression right = frag.getInitializer();
|
||||||
|
if (variableTracer != null && constantExtractor != null) {
|
||||||
|
List<Expression> tracedRights = variableTracer.traceVariableAll(right);
|
||||||
|
for (Expression tracedRight : tracedRights) {
|
||||||
|
if (tracedRight instanceof MethodInvocation mi) {
|
||||||
|
String calledName = mi.getName().getIdentifier();
|
||||||
|
String targetClass = context.getFqn(td);
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
if (mi.getExpression() == null) {
|
||||||
|
TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu);
|
||||||
|
if (targetTd != null) {
|
||||||
|
targetClass = context.getFqn(targetTd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(tracedRight, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ASTVisitor assignmentVisitor = new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression left = node.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
|
||||||
|
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
|
||||||
|
|
||||||
|
Expression right = node.getRightHandSide();
|
||||||
|
if (variableTracer != null && constantExtractor != null) {
|
||||||
|
List<Expression> tracedRights = variableTracer.traceVariableAll(right);
|
||||||
|
for (Expression tracedRight : tracedRights) {
|
||||||
|
if (tracedRight instanceof MethodInvocation mi) {
|
||||||
|
String calledName = mi.getName().getIdentifier();
|
||||||
|
String targetClass = context.getFqn(td);
|
||||||
|
|
||||||
|
if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
|
||||||
|
targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
|
||||||
|
} else if (mi.getExpression() instanceof SimpleName) {
|
||||||
|
String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
|
||||||
|
if (targetTd != null) {
|
||||||
|
targetClass = context.getFqn(targetTd);
|
||||||
|
}
|
||||||
|
} else if (mi.getExpression() == null) {
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu);
|
||||||
|
if (targetTd != null) {
|
||||||
|
targetClass = context.getFqn(targetTd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(tracedRight, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation node) {
|
||||||
|
processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited);
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation node) {
|
||||||
|
TypeDeclaration enclosingTd = findEnclosingType(node);
|
||||||
|
if (enclosingTd != null) {
|
||||||
|
String superFqn = context.getSuperclassFqn(enclosingTd);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.isConstructor() && md.getBody() != null) {
|
||||||
|
md.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object bodyDecl : td.bodyDeclarations()) {
|
||||||
|
if (bodyDecl instanceof Initializer init && init.getBody() != null) {
|
||||||
|
init.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
// Context-Aware Fallback (Up): Check superclass constructors
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
if (superTd != null) {
|
||||||
|
results.addAll(traceFieldInConstructors(superTd, fieldName, context, visited));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
// Context-Aware Fallback (Down): Check subclasses/implementations
|
||||||
|
List<String> impls = context.getImplementations(fqn);
|
||||||
|
if (impls != null) {
|
||||||
|
for (String implFqn : impls) {
|
||||||
|
TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
|
||||||
|
if (implTd != null) {
|
||||||
|
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
// Global Fallback: Search all parsed classes in the workspace
|
||||||
|
Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
|
||||||
|
if (allTypes != null) {
|
||||||
|
int matchedCount = 0;
|
||||||
|
for (TypeDeclaration typeDecl : allTypes) {
|
||||||
|
if (typeDecl == null) continue;
|
||||||
|
if (context.hasField(typeDecl, fieldName)) {
|
||||||
|
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited));
|
||||||
|
matchedCount++;
|
||||||
|
if (matchedCount >= 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.remove(cycleKey);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildFieldValuesFromCIC(
|
||||||
|
TypeDeclaration td,
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
CodebaseContext context,
|
||||||
|
RhsResolver rhsResolver,
|
||||||
|
Map<String, String> initialLocals) {
|
||||||
|
|
||||||
|
String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName);
|
||||||
|
if (actualTd == null) {
|
||||||
|
actualTd = td;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> fieldValues = new HashMap<>();
|
||||||
|
Set<String> visitedCtors = new HashSet<>();
|
||||||
|
buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors, rhsResolver, initialLocals);
|
||||||
|
return fieldValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildFieldValuesFromCIC(
|
||||||
|
TypeDeclaration td,
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
CodebaseContext context,
|
||||||
|
RhsResolver rhsResolver) {
|
||||||
|
return buildFieldValuesFromCIC(td, cic, context, rhsResolver, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildFieldValuesFromCIC(
|
||||||
|
TypeDeclaration td,
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
CodebaseContext context) {
|
||||||
|
return buildFieldValuesFromCIC(td, cic, context, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildFieldValuesFromCICRecursive(
|
||||||
|
TypeDeclaration td,
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
CodebaseContext context,
|
||||||
|
Map<String, String> fieldValues,
|
||||||
|
Set<String> visitedCtors,
|
||||||
|
RhsResolver rhsResolver,
|
||||||
|
Map<String, String> initialLocals) {
|
||||||
|
|
||||||
|
String tdKey = context.getFqn(td);
|
||||||
|
if (!visitedCtors.add(tdKey)) return;
|
||||||
|
|
||||||
|
IMethodBinding resolvedCtor = cic.resolveConstructorBinding();
|
||||||
|
|
||||||
|
for (MethodDeclaration ctor : td.getMethods()) {
|
||||||
|
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
|
||||||
|
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedCtor != null && ctor.resolveBinding() != null) {
|
||||||
|
matches = resolvedCtor.isEqualTo(ctor.resolveBinding()) || resolvedCtor.getKey().equals(ctor.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = ctor.parameters().size() == cic.arguments().size();
|
||||||
|
}
|
||||||
|
if (!matches) continue;
|
||||||
|
|
||||||
|
Map<String, String> paramValues = new HashMap<>();
|
||||||
|
for (int i = 0; i < ctor.parameters().size(); i++) {
|
||||||
|
String pName = ((SingleVariableDeclaration) ctor.parameters().get(i)).getName().getIdentifier();
|
||||||
|
Expression argExpr = (Expression) cic.arguments().get(i);
|
||||||
|
List<String> consts = new ArrayList<>();
|
||||||
|
if (constantExtractor != null) {
|
||||||
|
constantExtractor.extractConstantsFromArgument(argExpr, consts);
|
||||||
|
}
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
paramValues.put(pName, consts.get(0));
|
||||||
|
} else {
|
||||||
|
String resolved = null;
|
||||||
|
if (argExpr instanceof SimpleName sn && initialLocals != null && initialLocals.containsKey(sn.getIdentifier())) {
|
||||||
|
resolved = initialLocals.get(sn.getIdentifier());
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
resolved = constantResolver.resolve(argExpr, context);
|
||||||
|
}
|
||||||
|
if (resolved != null) paramValues.put(pName, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paramValues.isEmpty()) continue;
|
||||||
|
|
||||||
|
ctor.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression lhs = node.getLeftHandSide();
|
||||||
|
Expression rhs = node.getRightHandSide();
|
||||||
|
|
||||||
|
String fieldName = null;
|
||||||
|
if (lhs instanceof FieldAccess fa
|
||||||
|
&& fa.getExpression() instanceof ThisExpression) {
|
||||||
|
fieldName = fa.getName().getIdentifier();
|
||||||
|
} else if (lhs instanceof SimpleName sn
|
||||||
|
&& !paramValues.containsKey(sn.getIdentifier())) {
|
||||||
|
fieldName = sn.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldName != null) {
|
||||||
|
String paramVal = null;
|
||||||
|
if (rhs instanceof SimpleName snRhs) {
|
||||||
|
paramVal = paramValues.get(snRhs.getIdentifier());
|
||||||
|
} else if (rhsResolver != null) {
|
||||||
|
paramVal = rhsResolver.resolve(rhs, paramValues, td);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramVal != null) {
|
||||||
|
fieldValues.put(fieldName, paramVal);
|
||||||
|
fieldValues.put("this." + fieldName, paramVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctor.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation sci) {
|
||||||
|
IMethodBinding binding = sci.resolveConstructorBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
|
||||||
|
if (superTd != null) {
|
||||||
|
processSuperConstructorArgs(superTd, binding, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
if (superTd != null) {
|
||||||
|
processSuperConstructorArgs(superTd, null, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation ci) {
|
||||||
|
IMethodBinding binding = ci.resolveConstructorBinding();
|
||||||
|
processSuperConstructorArgs(td, binding, ci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(ci);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processSuperConstructorArgs(
|
||||||
|
TypeDeclaration superTd,
|
||||||
|
IMethodBinding resolvedBinding,
|
||||||
|
List<?> superArgs,
|
||||||
|
Map<String, String> subClassParamValues,
|
||||||
|
CodebaseContext context,
|
||||||
|
Map<String, String> fieldValues,
|
||||||
|
Set<String> visitedCtors,
|
||||||
|
RhsResolver rhsResolver) {
|
||||||
|
|
||||||
|
if (superTd == null) return;
|
||||||
|
|
||||||
|
for (MethodDeclaration superCtor : superTd.getMethods()) {
|
||||||
|
if (!superCtor.isConstructor() || superCtor.getBody() == null) continue;
|
||||||
|
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && superCtor.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(superCtor.resolveBinding()) || resolvedBinding.getKey().equals(superCtor.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = superCtor.parameters().size() == superArgs.size();
|
||||||
|
}
|
||||||
|
if (!matches) continue;
|
||||||
|
|
||||||
|
Map<String, String> superParamValues = new HashMap<>();
|
||||||
|
for (int i = 0; i < superCtor.parameters().size(); i++) {
|
||||||
|
String pName = ((SingleVariableDeclaration) superCtor.parameters().get(i)).getName().getIdentifier();
|
||||||
|
Expression argExpr = (Expression) superArgs.get(i);
|
||||||
|
|
||||||
|
String resolved = null;
|
||||||
|
if (argExpr instanceof SimpleName sn) {
|
||||||
|
resolved = subClassParamValues.get(sn.getIdentifier());
|
||||||
|
}
|
||||||
|
if (resolved == null) {
|
||||||
|
List<String> consts = new ArrayList<>();
|
||||||
|
if (constantExtractor != null) {
|
||||||
|
constantExtractor.extractConstantsFromArgument(argExpr, consts);
|
||||||
|
}
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
resolved = consts.get(0);
|
||||||
|
} else {
|
||||||
|
resolved = constantResolver.resolve(argExpr, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resolved != null) {
|
||||||
|
superParamValues.put(pName, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (superParamValues.isEmpty()) continue;
|
||||||
|
|
||||||
|
superCtor.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression lhs = node.getLeftHandSide();
|
||||||
|
Expression rhs = node.getRightHandSide();
|
||||||
|
|
||||||
|
String fieldName = null;
|
||||||
|
if (lhs instanceof FieldAccess fa
|
||||||
|
&& fa.getExpression() instanceof ThisExpression) {
|
||||||
|
fieldName = fa.getName().getIdentifier();
|
||||||
|
} else if (lhs instanceof SimpleName sn
|
||||||
|
&& !superParamValues.containsKey(sn.getIdentifier())) {
|
||||||
|
fieldName = sn.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldName != null) {
|
||||||
|
String paramVal = null;
|
||||||
|
if (rhs instanceof SimpleName snRhs) {
|
||||||
|
paramVal = superParamValues.get(snRhs.getIdentifier());
|
||||||
|
} else if (rhsResolver != null) {
|
||||||
|
paramVal = rhsResolver.resolve(rhs, superParamValues, superTd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramVal != null) {
|
||||||
|
fieldValues.put(fieldName, paramVal);
|
||||||
|
fieldValues.put("this." + fieldName, paramVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
superCtor.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation sci) {
|
||||||
|
IMethodBinding binding = sci.resolveConstructorBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
TypeDeclaration nextSuperTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
|
||||||
|
if (nextSuperTd != null) {
|
||||||
|
processSuperConstructorArgs(nextSuperTd, binding, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String superFqn = context.getSuperclassFqn(superTd);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
|
||||||
|
if (nextSuperTd != null) {
|
||||||
|
processSuperConstructorArgs(nextSuperTd, null, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation ci) {
|
||||||
|
IMethodBinding binding = ci.resolveConstructorBinding();
|
||||||
|
processSuperConstructorArgs(superTd, binding, ci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(ci);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String evaluateMethodCallInConstructor(
|
||||||
|
Expression rhs,
|
||||||
|
Map<String, String> paramValues,
|
||||||
|
TypeDeclaration td) {
|
||||||
|
|
||||||
|
if (!(rhs instanceof MethodInvocation mi)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if (receiver != null && !(receiver instanceof ThisExpression)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration method = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (method == null || method.getBody() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> localVars = new HashMap<>(paramValues);
|
||||||
|
return constantResolver.evaluateMethodBodyWithLocals(method, localVars, context, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClassInstanceCreation findReturnedCIC(
|
||||||
|
String className, String methodName, CodebaseContext context) {
|
||||||
|
return findReturnedCIC(className, methodName, context, new HashSet<>(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClassInstanceCreation findReturnedCIC(
|
||||||
|
String className, String methodName, CodebaseContext context,
|
||||||
|
Set<String> visited, int depth) {
|
||||||
|
if (depth > 50 || !visited.add(className + "#" + methodName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
ClassInstanceCreation[] result = {null};
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement rs) {
|
||||||
|
if (rs.getExpression() instanceof ClassInstanceCreation cic) {
|
||||||
|
result[0] = cic;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (result[0] != null) return result[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> impls = context.getImplementations(className);
|
||||||
|
for (String impl : impls) {
|
||||||
|
ClassInstanceCreation cic = findReturnedCIC(impl, methodName, context, visited, depth + 1);
|
||||||
|
if (cic != null) return cic;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveInlineInstantiationGetterResult(
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
String getterName,
|
||||||
|
CodebaseContext context,
|
||||||
|
Set<String> visited) {
|
||||||
|
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
|
CompilationUnit contextCu = cic.getRoot() instanceof CompilationUnit ? (CompilationUnit) cic.getRoot() : null;
|
||||||
|
AbstractTypeDeclaration atd = contextCu != null ? context.getAbstractTypeDeclaration(typeName, contextCu) : context.getAbstractTypeDeclaration(typeName);
|
||||||
|
if (atd == null) atd = context.getAbstractTypeDeclaration(typeName);
|
||||||
|
if (atd == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
if (atd instanceof RecordDeclaration rd) {
|
||||||
|
int compIdx = -1;
|
||||||
|
List<?> components = rd.recordComponents();
|
||||||
|
for (int i = 0; i < components.size(); i++) {
|
||||||
|
Object compObj = components.get(i);
|
||||||
|
if (compObj != null) {
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method getNameMethod = compObj.getClass().getMethod("getName");
|
||||||
|
SimpleName nameObj = (SimpleName) getNameMethod.invoke(compObj);
|
||||||
|
if (nameObj != null && nameObj.getIdentifier().equals(getterName)) {
|
||||||
|
compIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore reflection errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (compIdx >= 0 && compIdx < cic.arguments().size()) {
|
||||||
|
Expression argExpr = (Expression) cic.arguments().get(compIdx);
|
||||||
|
List<String> consts = new ArrayList<>();
|
||||||
|
if (constantExtractor != null) {
|
||||||
|
constantExtractor.extractConstantsFromArgument(argExpr, consts);
|
||||||
|
}
|
||||||
|
if (!consts.isEmpty()) {
|
||||||
|
return consts;
|
||||||
|
} else {
|
||||||
|
String resolved = constantResolver.resolve(argExpr, context);
|
||||||
|
if (resolved != null) {
|
||||||
|
return List.of(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(atd instanceof TypeDeclaration td)) return Collections.emptyList();
|
||||||
|
|
||||||
|
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
|
||||||
|
if (getter == null || getter.getBody() == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
String getterKey = typeName + "#" + getterName;
|
||||||
|
if (!visited.add(getterKey)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context);
|
||||||
|
if (fieldValues.isEmpty()) return Collections.emptyList();
|
||||||
|
|
||||||
|
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
||||||
|
return result != null ? List.of(result) : Collections.emptyList();
|
||||||
|
} finally {
|
||||||
|
visited.remove(getterKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) {
|
||||||
|
boolean isAllArgsConstructor = false;
|
||||||
|
boolean isRequiredArgsConstructor = false;
|
||||||
|
boolean isData = false;
|
||||||
|
boolean isValue = false;
|
||||||
|
|
||||||
|
for (Object modObj : td.modifiers()) {
|
||||||
|
if (modObj instanceof MarkerAnnotation ma) {
|
||||||
|
String name = ma.getTypeName().getFullyQualifiedName();
|
||||||
|
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
|
||||||
|
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
|
||||||
|
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
|
||||||
|
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
|
||||||
|
} else if (modObj instanceof NormalAnnotation na) {
|
||||||
|
String name = na.getTypeName().getFullyQualifiedName();
|
||||||
|
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
|
||||||
|
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
|
||||||
|
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
|
||||||
|
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAllArgsConstructor && !isRequiredArgsConstructor && !isData && !isValue) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
boolean isStatic = false;
|
||||||
|
boolean isFinal = false;
|
||||||
|
boolean hasNonNull = false;
|
||||||
|
for (Object modObj : fd.modifiers()) {
|
||||||
|
if (modObj instanceof Modifier mod) {
|
||||||
|
if (mod.isStatic()) isStatic = true;
|
||||||
|
if (mod.isFinal()) isFinal = true;
|
||||||
|
} else if (modObj instanceof MarkerAnnotation ma) {
|
||||||
|
if (ma.getTypeName().getFullyQualifiedName().endsWith("NonNull")) hasNonNull = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isStatic) continue;
|
||||||
|
|
||||||
|
if (isRequiredArgsConstructor && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
|
||||||
|
if (isData && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
|
||||||
|
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(fieldName)) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processConstructorInvocationArgs(List<?> arguments, TypeDeclaration targetTd, ASTNode callNode, List<String> results, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
|
if (targetTd == null) return;
|
||||||
|
MethodDeclaration callerMd = findEnclosingMethod(callNode);
|
||||||
|
|
||||||
|
IMethodBinding resolvedBinding = null;
|
||||||
|
if (callNode instanceof ConstructorInvocation ci) {
|
||||||
|
resolvedBinding = ci.resolveConstructorBinding();
|
||||||
|
} else if (callNode instanceof SuperConstructorInvocation sci) {
|
||||||
|
resolvedBinding = sci.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MethodDeclaration otherMd : targetTd.getMethods()) {
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new HashSet<>());
|
||||||
|
if (targetIdx >= 0 && targetIdx < arguments.size()) {
|
||||||
|
Expression arg = (Expression) arguments.get(targetIdx);
|
||||||
|
String val = constantResolver.resolve(arg, context);
|
||||||
|
if (val != null) {
|
||||||
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
|
if (constantExtractor != null) {
|
||||||
|
results.add(constantExtractor.parseEnumSetElement(eVal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, Set<MethodDeclaration> visited) {
|
||||||
|
if (constructorMd == null || constructorMd.getBody() == null) return -1;
|
||||||
|
if (!visited.add(constructorMd)) return -1;
|
||||||
|
|
||||||
|
final int[] foundIdx = {-1};
|
||||||
|
constructorMd.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment asn) {
|
||||||
|
Expression left = asn.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) ||
|
||||||
|
(left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) ||
|
||||||
|
(left instanceof SuperFieldAccess sfa && sfa.getName().getIdentifier().equals(fieldName))) {
|
||||||
|
Expression right = asn.getRightHandSide();
|
||||||
|
String rightName = extractVariableName(right);
|
||||||
|
if (rightName != null) {
|
||||||
|
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||||
|
if (svd.getName().getIdentifier().equals(rightName)) {
|
||||||
|
foundIdx[0] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(asn);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(ConstructorInvocation node) {
|
||||||
|
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
|
||||||
|
TypeDeclaration enclosingTd = findEnclosingType(node);
|
||||||
|
if (enclosingTd != null) {
|
||||||
|
for (MethodDeclaration otherMd : enclosingTd.getMethods()) {
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size();
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
|
||||||
|
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
|
||||||
|
Expression arg = (Expression) node.arguments().get(targetIdx);
|
||||||
|
String argName = extractVariableName(arg);
|
||||||
|
if (argName != null) {
|
||||||
|
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||||
|
if (svd.getName().getIdentifier().equals(argName)) {
|
||||||
|
foundIdx[0] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperConstructorInvocation node) {
|
||||||
|
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
|
||||||
|
TypeDeclaration enclosingTd = findEnclosingType(node);
|
||||||
|
if (enclosingTd != null) {
|
||||||
|
String superFqn = context.getSuperclassFqn(enclosingTd);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
if (superTd != null) {
|
||||||
|
for (MethodDeclaration otherMd : superTd.getMethods()) {
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size();
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
|
||||||
|
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
|
||||||
|
Expression arg = (Expression) node.arguments().get(targetIdx);
|
||||||
|
String argName = extractVariableName(arg);
|
||||||
|
if (argName != null) {
|
||||||
|
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||||
|
if (svd.getName().getIdentifier().equals(argName)) {
|
||||||
|
foundIdx[0] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return foundIdx[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractVariableName(Expression expr) {
|
||||||
|
if (expr instanceof SimpleName sn) return sn.getIdentifier();
|
||||||
|
if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier();
|
||||||
|
if (expr instanceof SuperFieldAccess sfa) return sfa.getName().getIdentifier();
|
||||||
|
if (expr instanceof CastExpression ce) return extractVariableName(ce.getExpression());
|
||||||
|
if (expr instanceof ParenthesizedExpression pe) return extractVariableName(pe.getExpression());
|
||||||
|
if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
|
||||||
|
if (!mi.arguments().isEmpty()) return extractVariableName((Expression)mi.arguments().get(0));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (MethodDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (TypeDeclaration) parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class DynamicClasspathResolver {
|
||||||
|
|
||||||
|
public List<String> resolveClasspath(Path projectRoot) {
|
||||||
|
log.info("Attempting dynamic classpath resolution for project at {}", projectRoot);
|
||||||
|
if (projectRoot == null || !Files.exists(projectRoot)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(projectRoot.resolve("pom.xml"))) {
|
||||||
|
return resolveMaven(projectRoot);
|
||||||
|
} else if (Files.exists(projectRoot.resolve("build.gradle")) || Files.exists(projectRoot.resolve("build.gradle.kts"))) {
|
||||||
|
return resolveGradle(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("No Maven or Gradle configuration found at {}. Proceeding without dynamic classpath.", projectRoot);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> resolveMaven(Path projectRoot) {
|
||||||
|
log.info("Maven project detected. Resolving dependencies...");
|
||||||
|
Path cpFile = null;
|
||||||
|
try {
|
||||||
|
cpFile = Files.createTempFile("state_machine_exporter_cp", ".txt");
|
||||||
|
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
|
||||||
|
if (mvnCmd == null) {
|
||||||
|
log.warn("Maven executable not found.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
mvnCmd,
|
||||||
|
"-q",
|
||||||
|
"dependency:build-classpath",
|
||||||
|
"-DincludeScope=compile",
|
||||||
|
"-Dmdep.outputFile=" + cpFile.toAbsolutePath().toString()
|
||||||
|
);
|
||||||
|
pb.directory(projectRoot.toFile());
|
||||||
|
|
||||||
|
runProcess(pb);
|
||||||
|
|
||||||
|
if (Files.exists(cpFile)) {
|
||||||
|
String cpString = Files.readString(cpFile).trim();
|
||||||
|
if (!cpString.isEmpty()) {
|
||||||
|
return Arrays.asList(cpString.split(File.pathSeparator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to dynamically resolve Maven classpath", e);
|
||||||
|
} finally {
|
||||||
|
if (cpFile != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(cpFile);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> resolveGradle(Path projectRoot) {
|
||||||
|
log.info("Gradle project detected. Resolving dependencies...");
|
||||||
|
Path initScript = null;
|
||||||
|
try {
|
||||||
|
initScript = Files.createTempFile("state_machine_exporter_init", ".gradle");
|
||||||
|
String gradleCmd = getCommand(projectRoot, "gradlew", "gradle");
|
||||||
|
if (gradleCmd == null) {
|
||||||
|
log.warn("Gradle executable not found.");
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String scriptContent = """
|
||||||
|
allprojects {
|
||||||
|
task smePrintClasspath {
|
||||||
|
doLast {
|
||||||
|
def cp = []
|
||||||
|
if (configurations.findByName("compileClasspath")) {
|
||||||
|
cp = configurations.compileClasspath.files
|
||||||
|
}
|
||||||
|
println "SME_CLASSPATH_MARKER:" + cp.join(File.pathSeparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(initScript, scriptContent);
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
gradleCmd,
|
||||||
|
"-q",
|
||||||
|
"-I", initScript.toAbsolutePath().toString(),
|
||||||
|
"smePrintClasspath"
|
||||||
|
);
|
||||||
|
pb.directory(projectRoot.toFile());
|
||||||
|
|
||||||
|
String output = runProcessAndGetOutput(pb);
|
||||||
|
for (String line : output.split("\\r?\\n")) {
|
||||||
|
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
|
||||||
|
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
|
||||||
|
if (!cpString.isEmpty()) {
|
||||||
|
return Arrays.asList(cpString.split(File.pathSeparator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to dynamically resolve Gradle classpath", e);
|
||||||
|
} finally {
|
||||||
|
if (initScript != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(initScript);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCommand(Path projectRoot, String wrapper, String systemBinary) {
|
||||||
|
Path wrapperUnix = projectRoot.resolve(wrapper);
|
||||||
|
Path wrapperWin = projectRoot.resolve(wrapper + ".bat");
|
||||||
|
|
||||||
|
boolean isWin = System.getProperty("os.name").toLowerCase().contains("win");
|
||||||
|
|
||||||
|
if (isWin && Files.exists(wrapperWin)) return wrapperWin.toAbsolutePath().toString();
|
||||||
|
if (!isWin && Files.exists(wrapperUnix)) {
|
||||||
|
// Need absolute path like ./gradlew
|
||||||
|
return wrapperUnix.toAbsolutePath().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void runProcess(ProcessBuilder pb) throws IOException, InterruptedException {
|
||||||
|
Process p = pb.start();
|
||||||
|
p.waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String runProcessAndGetOutput(ProcessBuilder pb) throws IOException, InterruptedException {
|
||||||
|
Process p = pb.start();
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
out.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.waitFor();
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EnrichmentService {
|
||||||
|
private final List<AnalysisEnricher> enrichers;
|
||||||
|
|
||||||
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
|
for (AnalysisEnricher enricher : enrichers) {
|
||||||
|
enricher.enrich(result, context, intelligence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,638 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GenericEventDetector {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
private final List<LibraryHint> hints;
|
||||||
|
|
||||||
|
public List<TriggerPoint> detect(CompilationUnit cu) {
|
||||||
|
List<TriggerPoint> triggers = new ArrayList<>();
|
||||||
|
String fileName = "unknown";
|
||||||
|
try {
|
||||||
|
if (cu.getJavaElement() != null) fileName = cu.getJavaElement().getElementName();
|
||||||
|
} catch (Exception e) {}
|
||||||
|
|
||||||
|
log.debug("Scanning for triggers in {}", fileName);
|
||||||
|
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processSendEventMethodReference(MethodInvocation node, ExpressionMethodReference emr, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||||
|
Expression receiver = node.getExpression();
|
||||||
|
String eventValue = extractEventFromReceiver(receiver);
|
||||||
|
if (eventValue == null) {
|
||||||
|
eventValue = receiver != null ? receiver.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
|
TypeDeclaration type = findEnclosingType(node);
|
||||||
|
if (type == null) return;
|
||||||
|
|
||||||
|
String sourceState = extractSourceState(node);
|
||||||
|
String[] smTypes = resolveStateMachineTypeArgumentsForExpression(emr.getExpression(), node);
|
||||||
|
|
||||||
|
if (eventValue != null) {
|
||||||
|
triggers.add(TriggerPoint.builder()
|
||||||
|
.event(eventValue)
|
||||||
|
.sourceState(sourceState)
|
||||||
|
.className(context.getFqn(type))
|
||||||
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.stateTypeFqn(smTypes[0])
|
||||||
|
.eventTypeFqn(smTypes[1])
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractEventFromReceiver(Expression receiver) {
|
||||||
|
if (receiver == null) return null;
|
||||||
|
if (receiver instanceof MethodInvocation mi) {
|
||||||
|
String mName = mi.getName().getIdentifier();
|
||||||
|
if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
String resolved = constantResolver.resolve(arg, context);
|
||||||
|
return resolved != null ? resolved : arg.toString();
|
||||||
|
}
|
||||||
|
if (mi.getExpression() != null) {
|
||||||
|
return extractEventFromReceiver(mi.getExpression());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] resolveStateMachineTypeArgumentsForExpression(Expression receiver, MethodInvocation node) {
|
||||||
|
if (receiver == null) return new String[]{null, null};
|
||||||
|
|
||||||
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding smBinding = findStateMachineSupertype(binding, new java.util.HashSet<>());
|
||||||
|
if (smBinding != null) {
|
||||||
|
ITypeBinding[] typeArgs = smBinding.getTypeArguments();
|
||||||
|
if (typeArgs.length >= 2) {
|
||||||
|
return new String[]{typeArgs[0].getQualifiedName(), typeArgs[1].getQualifiedName()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Type declType = findReceiverTypeManually(receiver, node);
|
||||||
|
if (declType instanceof ParameterizedType pt) {
|
||||||
|
List<?> typeArgs = pt.typeArguments();
|
||||||
|
if (typeArgs.size() >= 2) {
|
||||||
|
CompilationUnit compilationUnit = (CompilationUnit) node.getRoot();
|
||||||
|
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), compilationUnit);
|
||||||
|
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), compilationUnit);
|
||||||
|
return new String[]{stateType, eventType};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[]{null, null};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||||
|
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, null);
|
||||||
|
if (builtTriggers != null) {
|
||||||
|
for (TriggerPoint trigger : builtTriggers) {
|
||||||
|
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
||||||
|
triggers.add(trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processHints(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||||
|
if (hints == null || hints.isEmpty()) return;
|
||||||
|
|
||||||
|
String calledMethod = resolveCalledMethodName(node);
|
||||||
|
if (calledMethod == null) return;
|
||||||
|
|
||||||
|
for (LibraryHint hint : hints) {
|
||||||
|
if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
|
||||||
|
String eventToUse = hint.getEvent();
|
||||||
|
if (eventToUse == null && hint.getEventArgumentIndex() != null) {
|
||||||
|
if (node.arguments().size() > hint.getEventArgumentIndex()) {
|
||||||
|
Expression argExpr = (Expression) node.arguments().get(hint.getEventArgumentIndex());
|
||||||
|
eventToUse = argExpr.toString();
|
||||||
|
if (hint.getEventArgumentMethod() != null && !hint.getEventArgumentMethod().isEmpty()) {
|
||||||
|
eventToUse = eventToUse + "." + hint.getEventArgumentMethod() + "()";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TriggerPoint> builtTriggers = buildTriggerPoints(node, cu, eventToUse);
|
||||||
|
if (builtTriggers != null) {
|
||||||
|
for (TriggerPoint trigger : builtTriggers) {
|
||||||
|
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||||
|
triggers.add(trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHintMatch(String called, String hintFqn) {
|
||||||
|
return hintFqn.endsWith("." + called);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCalledMethodName(MethodInvocation node) {
|
||||||
|
Expression receiver = node.getExpression();
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
if (receiver == null) return methodName;
|
||||||
|
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
String name = sn.getIdentifier();
|
||||||
|
// Check if it's a field in the enclosing class
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(name)) {
|
||||||
|
String typeName = fd.getType().toString();
|
||||||
|
return typeName + "." + methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name + "." + methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return receiver.toString() + "." + methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TriggerPoint> buildTriggerPoints(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
|
||||||
|
String eventValue = null;
|
||||||
|
if (forcedEvent != null) {
|
||||||
|
eventValue = context.resolveString(forcedEvent);
|
||||||
|
} else {
|
||||||
|
if (node.arguments().isEmpty()) return Collections.emptyList();
|
||||||
|
Expression eventExpr = (Expression) node.arguments().get(0);
|
||||||
|
|
||||||
|
// Try MessageBuilder extraction first
|
||||||
|
eventValue = extractEventFromMessageBuilder(eventExpr);
|
||||||
|
|
||||||
|
if (eventValue != null) {
|
||||||
|
eventValue = context.resolveString(eventValue);
|
||||||
|
} else {
|
||||||
|
eventValue = context.resolveExpression(eventExpr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
|
TypeDeclaration type = findEnclosingType(node);
|
||||||
|
|
||||||
|
if (type == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
String sourceState = extractSourceState(node);
|
||||||
|
String[] smTypes = resolveStateMachineTypeArguments(node);
|
||||||
|
|
||||||
|
List<TriggerPoint> results = new ArrayList<>();
|
||||||
|
if (eventValue != null && eventValue.startsWith("ENUM_SET:")) {
|
||||||
|
String[] parts = eventValue.substring(9).split(",");
|
||||||
|
for (String part : parts) {
|
||||||
|
if (!part.trim().isEmpty()) {
|
||||||
|
results.add(TriggerPoint.builder()
|
||||||
|
.event(part.trim())
|
||||||
|
.sourceState(sourceState)
|
||||||
|
.className(context.getFqn(type))
|
||||||
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.stateTypeFqn(smTypes[0])
|
||||||
|
.eventTypeFqn(smTypes[1])
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.add(TriggerPoint.builder()
|
||||||
|
.event(eventValue)
|
||||||
|
.sourceState(sourceState)
|
||||||
|
.className(context.getFqn(type))
|
||||||
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(type)))
|
||||||
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.stateTypeFqn(smTypes[0])
|
||||||
|
.eventTypeFqn(smTypes[1])
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractSourceState(ASTNode node) {
|
||||||
|
ASTNode current = node;
|
||||||
|
while (current != null && !(current instanceof MethodDeclaration)) {
|
||||||
|
ASTNode parent = current.getParent();
|
||||||
|
|
||||||
|
// 1. Direct parent is IfStatement wrapper (e.g., if (state == X) { sm.sendEvent() })
|
||||||
|
if (parent instanceof IfStatement ifStmt) {
|
||||||
|
// Only consider if our current node is in the 'then' or 'else' part, not the condition
|
||||||
|
if (ifStmt.getExpression() != current) {
|
||||||
|
Expression expr = ifStmt.getExpression();
|
||||||
|
String state = extractStateFromExpression(expr);
|
||||||
|
if (state != null) return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parent is a Block or SwitchStatement: perform sibling traversal
|
||||||
|
if (parent instanceof Block block) {
|
||||||
|
String state = extractStateFromSiblings(current, block.statements());
|
||||||
|
if (state != null) return state;
|
||||||
|
} else if (parent instanceof SwitchStatement switchStmt) {
|
||||||
|
String state = extractStateFromSiblings(current, switchStmt.statements());
|
||||||
|
if (state != null) return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractStateFromSiblings(ASTNode currentNode, List<?> statements) {
|
||||||
|
int index = statements.indexOf(currentNode);
|
||||||
|
if (index <= 0) return null; // No previous siblings
|
||||||
|
|
||||||
|
// Walk backwards through siblings
|
||||||
|
for (int i = index - 1; i >= 0; i--) {
|
||||||
|
Object stmtObj = statements.get(i);
|
||||||
|
|
||||||
|
if (stmtObj instanceof SwitchCase switchCase) {
|
||||||
|
if (!switchCase.expressions().isEmpty()) {
|
||||||
|
return getSimpleNameString((Expression) switchCase.expressions().get(0));
|
||||||
|
}
|
||||||
|
} else if (stmtObj instanceof IfStatement ifStmt) {
|
||||||
|
// Guard clause detection: check if the 'then' block has a return/throw
|
||||||
|
if (isGuardClause(ifStmt.getThenStatement())) {
|
||||||
|
Expression expr = ifStmt.getExpression();
|
||||||
|
// If it's a guard clause, it usually checks `state != EXPECTED`, so the true state *is* EXPECTED
|
||||||
|
String state = extractStateFromExpression(expr);
|
||||||
|
if (state != null) return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGuardClause(Statement thenStatement) {
|
||||||
|
if (thenStatement instanceof ReturnStatement || thenStatement instanceof ThrowStatement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (thenStatement instanceof Block block) {
|
||||||
|
for (Object obj : block.statements()) {
|
||||||
|
if (obj instanceof ReturnStatement || obj instanceof ThrowStatement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractStateFromExpression(Expression expr) {
|
||||||
|
if (expr instanceof InfixExpression infix) {
|
||||||
|
if (infix.getOperator() == InfixExpression.Operator.EQUALS || infix.getOperator() == InfixExpression.Operator.NOT_EQUALS) {
|
||||||
|
Expression left = infix.getLeftOperand();
|
||||||
|
Expression right = infix.getRightOperand();
|
||||||
|
|
||||||
|
// Ignore null checks entirely
|
||||||
|
if (left instanceof NullLiteral || right instanceof NullLiteral) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usually one is a method call like getState() or a variable like `state`
|
||||||
|
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
|
||||||
|
|
||||||
|
// If one is a QualifiedName (enum constant) or StringLiteral, it's likely the state
|
||||||
|
if (left instanceof QualifiedName || left instanceof StringLiteral || left instanceof FieldAccess) {
|
||||||
|
return getSimpleNameString(left);
|
||||||
|
}
|
||||||
|
if (right instanceof QualifiedName || right instanceof StringLiteral || right instanceof FieldAccess) {
|
||||||
|
return getSimpleNameString(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
return getSimpleNameString(right);
|
||||||
|
}
|
||||||
|
} else if (expr instanceof MethodInvocation mi) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSimpleNameString(Expression expr) {
|
||||||
|
if (expr instanceof QualifiedName qn) {
|
||||||
|
return qn.getName().getIdentifier();
|
||||||
|
} else if (expr instanceof FieldAccess fa) {
|
||||||
|
return fa.getName().getIdentifier();
|
||||||
|
} else if (expr instanceof StringLiteral sl) {
|
||||||
|
return sl.getLiteralValue();
|
||||||
|
}
|
||||||
|
return expr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractEventFromMessageBuilder(Expression expr) {
|
||||||
|
if (expr instanceof CastExpression ce) {
|
||||||
|
return extractEventFromMessageBuilder(ce.getExpression());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
String varName = sn.getIdentifier();
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
// Find variable declaration
|
||||||
|
final Expression[] initializer = new Expression[1];
|
||||||
|
enclosingMethod.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
initializer[0] = node.getInitializer();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
initializer[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (initializer[0] != null) {
|
||||||
|
return extractEventFromMessageBuilder(initializer[0]); // recursive
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it has NO initializer, it might be a method parameter!
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return varName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expr instanceof ClassInstanceCreation cic) {
|
||||||
|
return cic.getType().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(expr instanceof MethodInvocation mi)) return null;
|
||||||
|
|
||||||
|
// Trace back chain: MessageBuilder.withPayload(event).setHeader(...).build()
|
||||||
|
MethodInvocation current = mi;
|
||||||
|
while (current != null) {
|
||||||
|
String name = current.getName().getIdentifier();
|
||||||
|
if (("withPayload".equals(name) || "just".equals(name)) && !current.arguments().isEmpty()) {
|
||||||
|
Expression payloadExpr = (Expression) current.arguments().get(0);
|
||||||
|
|
||||||
|
// If it's Mono.just(msg), where msg is a variable
|
||||||
|
if ("just".equals(name)) {
|
||||||
|
String extracted = extractEventFromMessageBuilder(payloadExpr);
|
||||||
|
if (extracted != null) return extracted;
|
||||||
|
}
|
||||||
|
|
||||||
|
String resolved = constantResolver.resolve(payloadExpr, context);
|
||||||
|
if (resolved != null) return resolved;
|
||||||
|
|
||||||
|
// If not a constant, it might be a parameter like 'event'
|
||||||
|
if (payloadExpr instanceof CastExpression ce) {
|
||||||
|
payloadExpr = ce.getExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadExpr instanceof SimpleName sn) {
|
||||||
|
String extracted = extractEventFromMessageBuilder(payloadExpr);
|
||||||
|
if (extracted != null) {
|
||||||
|
return extracted;
|
||||||
|
}
|
||||||
|
return sn.getIdentifier(); // Fall back to returning the parameter name
|
||||||
|
}
|
||||||
|
return payloadExpr.toString();
|
||||||
|
} else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
|
||||||
|
// If the event is obtained by calling a method on a provider/supplier parameter,
|
||||||
|
// return the provider's name so CallGraphBuilder can trace the lambda argument
|
||||||
|
String traced = extractEventFromMessageBuilder(sn);
|
||||||
|
return traced != null ? traced : sn.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression receiver = current.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation nextMi) {
|
||||||
|
current = nextMi;
|
||||||
|
} else {
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||||
|
protected String currentMethodFqn;
|
||||||
|
protected Map<String, List<CallEdge>> graph;
|
||||||
|
|
||||||
|
public HeuristicCallGraphEngine(CodebaseContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String, List<CallEdge>> buildCallGraph() {
|
||||||
|
graph = new HashMap<>();
|
||||||
|
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
MethodDeclaration md = findEnclosingMethod(node);
|
||||||
|
if (md != null) {
|
||||||
|
TypeDeclaration td = findEnclosingType(md);
|
||||||
|
if (td != null) {
|
||||||
|
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||||
|
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||||
|
List<String> args = resolveArguments(node.arguments());
|
||||||
|
for (String calledMethod : calledMethods) {
|
||||||
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
|
||||||
|
}
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
if (argObj instanceof ExpressionMethodReference emr) {
|
||||||
|
String typeName = emr.getExpression().toString();
|
||||||
|
if ("this".equals(typeName) || "super".equals(typeName)) {
|
||||||
|
TypeDeclaration td2 = findEnclosingType(node);
|
||||||
|
if (td2 != null) {
|
||||||
|
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||||
|
if (refMethod != null) {
|
||||||
|
List<String> implicitArgs = new ArrayList<>();
|
||||||
|
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||||
|
implicitArgs.add(node.getExpression().toString());
|
||||||
|
} else {
|
||||||
|
implicitArgs.addAll(args);
|
||||||
|
}
|
||||||
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs, receiver));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String fallbackTypeFqn = null;
|
||||||
|
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
fallbackTypeFqn = binding.getQualifiedName();
|
||||||
|
} else if (emr.getExpression() instanceof SimpleName sn) {
|
||||||
|
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||||
|
}
|
||||||
|
if (fallbackTypeFqn != null) {
|
||||||
|
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||||
|
List<String> implicitArgs = new ArrayList<>();
|
||||||
|
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||||
|
implicitArgs.add(node.getExpression().toString());
|
||||||
|
} else {
|
||||||
|
implicitArgs.addAll(args);
|
||||||
|
}
|
||||||
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs, receiver));
|
||||||
|
|
||||||
|
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||||
|
for (String impl : impls) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperMethodInvocation node) {
|
||||||
|
MethodDeclaration md = findEnclosingMethod(node);
|
||||||
|
if (md != null) {
|
||||||
|
TypeDeclaration tdOuter = findEnclosingType(md);
|
||||||
|
if (tdOuter != null) {
|
||||||
|
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
String calledMethod = null;
|
||||||
|
if (superTd != null) {
|
||||||
|
calledMethod = resolveMethodInType(superTd, methodName);
|
||||||
|
}
|
||||||
|
if (calledMethod == null) {
|
||||||
|
calledMethod = superFqn + "." + methodName;
|
||||||
|
}
|
||||||
|
List<String> args = resolveArguments(node.arguments());
|
||||||
|
String receiver = "super";
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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());
|
||||||
|
|
||||||
|
if (isThisOrSuperGetter) {
|
||||||
|
return originalExpr.toString(); // Preserve "this.getTransitionType()" or "super.getEvent()" for later substitution
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
String baseCalled = resolveCalledMethod(node);
|
||||||
|
if (baseCalled == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
List<String> allResolved = new ArrayList<>();
|
||||||
|
allResolved.add(baseCalled);
|
||||||
|
|
||||||
|
if (baseCalled.contains(".")) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 resolveMethodInTypeHierarchy(td, methodName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
return binding.getQualifiedName() + "." + methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiver instanceof ThisExpression) {
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td) + "." + methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||||
|
if (fallbackTypeFqn != null) {
|
||||||
|
return fallbackTypeFqn + "." + methodName;
|
||||||
|
}
|
||||||
|
String receiverName = sn.getIdentifier();
|
||||||
|
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) {
|
||||||
|
// Check parameters
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return resolveTypeToFqn(svd.getType(), receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check method body (local variables)
|
||||||
|
if (enclosingMethod.getBody() != null) {
|
||||||
|
Type[] foundType = new Type[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)) {
|
||||||
|
foundType[0] = node.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundType[0] != null) {
|
||||||
|
return resolveTypeToFqn(foundType[0], receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check fields in enclosing class and its superclasses
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||||
|
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)) {
|
||||||
|
return resolveTypeToFqn(field.getType(), receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String superclass = context.getSuperclassFqn(currentType);
|
||||||
|
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
||||||
|
if (type == null) return null;
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InterceptorDetector {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
public List<EntryPoint> detect(CompilationUnit cu) {
|
||||||
|
List<EntryPoint> entryPoints = new ArrayList<>();
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(TypeDeclaration node) {
|
||||||
|
if (isInterceptor(node)) {
|
||||||
|
processInterceptor(node, entryPoints);
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return entryPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInterceptor(TypeDeclaration node) {
|
||||||
|
if (node.getSuperclassType() != null) {
|
||||||
|
// Check for common adapter/base classes if needed
|
||||||
|
}
|
||||||
|
for (Object interfaceType : node.superInterfaceTypes()) {
|
||||||
|
String name = interfaceType.toString();
|
||||||
|
if (name.contains("HandlerInterceptor") || name.contains("Filter") || name.contains("WebRequestInterceptor")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processInterceptor(TypeDeclaration node, List<EntryPoint> entryPoints) {
|
||||||
|
String fqn = context.getFqn(node);
|
||||||
|
for (MethodDeclaration method : node.getMethods()) {
|
||||||
|
String methodName = method.getName().getIdentifier();
|
||||||
|
if (isInterceptorMethod(methodName)) {
|
||||||
|
Map<String, String> metadata = new HashMap<>();
|
||||||
|
metadata.put("interceptorType", getInterceptorType(node));
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.name("INTERCEPTOR: " + node.getName().getIdentifier() + "." + methodName)
|
||||||
|
.className(fqn)
|
||||||
|
.methodName(methodName)
|
||||||
|
.metadata(metadata)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInterceptorMethod(String name) {
|
||||||
|
return name.equals("preHandle") || name.equals("postHandle") || name.equals("afterCompletion") || name.equals("doFilter");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getInterceptorType(TypeDeclaration node) {
|
||||||
|
for (Object interfaceType : node.superInterfaceTypes()) {
|
||||||
|
String name = interfaceType.toString();
|
||||||
|
if (name.contains("HandlerInterceptor")) return "Spring MVC Interceptor";
|
||||||
|
if (name.contains("Filter")) return "Servlet Filter";
|
||||||
|
}
|
||||||
|
return "Generic Interceptor";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||||
|
private final InjectionPointAnalyzer injectionAnalyzer;
|
||||||
|
private String currentMethodFqn;
|
||||||
|
private Map<String, List<CallEdge>> graph;
|
||||||
|
|
||||||
|
public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) {
|
||||||
|
super(context);
|
||||||
|
this.injectionAnalyzer = injectionAnalyzer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String, List<CallEdge>> buildCallGraph() {
|
||||||
|
graph = new HashMap<>();
|
||||||
|
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
MethodDeclaration md = findEnclosingMethod(node);
|
||||||
|
if (md != null) {
|
||||||
|
TypeDeclaration td = findEnclosingType(md);
|
||||||
|
if (td != null) {
|
||||||
|
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||||
|
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||||
|
List<String> args = resolveArguments(node.arguments());
|
||||||
|
for (String calledMethod : calledMethods) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||||
|
}
|
||||||
|
for (Object argObj : node.arguments()) {
|
||||||
|
if (argObj instanceof ExpressionMethodReference emr) {
|
||||||
|
String typeName = emr.getExpression().toString();
|
||||||
|
if ("this".equals(typeName) || "super".equals(typeName)) {
|
||||||
|
TypeDeclaration td2 = findEnclosingType(node);
|
||||||
|
if (td2 != null) {
|
||||||
|
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||||
|
if (refMethod != null) {
|
||||||
|
List<String> implicitArgs = new ArrayList<>();
|
||||||
|
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||||
|
implicitArgs.add(node.getExpression().toString());
|
||||||
|
} else {
|
||||||
|
implicitArgs.addAll(args);
|
||||||
|
}
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String fallbackTypeFqn = null;
|
||||||
|
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
fallbackTypeFqn = binding.getQualifiedName();
|
||||||
|
} else if (emr.getExpression() instanceof SimpleName sn) {
|
||||||
|
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||||
|
}
|
||||||
|
if (fallbackTypeFqn != null) {
|
||||||
|
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||||
|
List<String> implicitArgs = new ArrayList<>();
|
||||||
|
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
|
||||||
|
implicitArgs.add(node.getExpression().toString());
|
||||||
|
} else {
|
||||||
|
implicitArgs.addAll(args);
|
||||||
|
}
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
|
||||||
|
|
||||||
|
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||||
|
for (String impl : impls) {
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SuperMethodInvocation node) {
|
||||||
|
MethodDeclaration md = findEnclosingMethod(node);
|
||||||
|
if (md != null) {
|
||||||
|
TypeDeclaration tdOuter = findEnclosingType(md);
|
||||||
|
if (tdOuter != null) {
|
||||||
|
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
|
String calledMethod = null;
|
||||||
|
if (superTd != null) {
|
||||||
|
calledMethod = resolveMethodInType(superTd, methodName);
|
||||||
|
}
|
||||||
|
if (calledMethod == null) {
|
||||||
|
calledMethod = superFqn + "." + methodName;
|
||||||
|
}
|
||||||
|
List<String> args = resolveArguments(node.arguments());
|
||||||
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
|
||||||
|
if (expr == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||||
|
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) {
|
||||||
|
String baseCalled = resolveCalledMethod(node);
|
||||||
|
if (baseCalled == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
List<String> allResolved = new ArrayList<>();
|
||||||
|
allResolved.add(baseCalled);
|
||||||
|
|
||||||
|
if (baseCalled.contains(".")) {
|
||||||
|
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||||
|
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
List<String> impls = context.getImplementations(className);
|
||||||
|
for (String impl : impls) {
|
||||||
|
allResolved.add(impl + "." + methodName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allResolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (injectionAnalyzer != null) {
|
||||||
|
IBinding nameBinding = null;
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
nameBinding = sn.resolveBinding();
|
||||||
|
} else if (receiver instanceof FieldAccess fa) {
|
||||||
|
nameBinding = fa.resolveFieldBinding();
|
||||||
|
}
|
||||||
|
if (nameBinding instanceof IVariableBinding varBinding) {
|
||||||
|
System.out.println("CALLGRAPH RESOLVING BEAN FOR BINDING: " + varBinding.getName() + " calling " + methodName);
|
||||||
|
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||||
|
if (concreteFqn != null) {
|
||||||
|
System.out.println(" -> RESOLVED CONCRETE FQN: " + concreteFqn);
|
||||||
|
return concreteFqn + "." + methodName;
|
||||||
|
} else {
|
||||||
|
System.out.println(" -> RESOLVE FAILED, RETURNED NULL");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.out.println("CALLGRAPH NAME BINDING IS NOT VARIABLE: " + nameBinding + " calling " + methodName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
String typeName = null;
|
||||||
|
try {
|
||||||
|
if (binding.getErasure() != null) {
|
||||||
|
typeName = binding.getErasure().getQualifiedName();
|
||||||
|
if (typeName == null || typeName.isEmpty()) {
|
||||||
|
typeName = binding.getErasure().getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore JDT internal exceptions
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeName == null || typeName.isEmpty()) {
|
||||||
|
typeName = binding.getQualifiedName();
|
||||||
|
if (typeName == null || typeName.isEmpty()) {
|
||||||
|
typeName = binding.getName();
|
||||||
|
}
|
||||||
|
if (typeName != null && typeName.contains("<")) {
|
||||||
|
typeName = typeName.replaceAll("<.*>", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeName != null && !typeName.isEmpty()) {
|
||||||
|
return typeName + "." + methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiver instanceof ThisExpression) {
|
||||||
|
TypeDeclaration td = findEnclosingType(node);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td) + "." + methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||||
|
if (fallbackTypeFqn != null) {
|
||||||
|
return fallbackTypeFqn + "." + methodName;
|
||||||
|
}
|
||||||
|
String receiverName = sn.getIdentifier();
|
||||||
|
return receiverName + "." + methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
||||||
|
String varName = receiverNameNode.getIdentifier();
|
||||||
|
|
||||||
|
// 1. Check local variables in enclosing method
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
// Check parameters
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return resolveTypeToFqn(svd.getType(), receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check method body (local variables)
|
||||||
|
if (enclosingMethod.getBody() != null) {
|
||||||
|
Type[] foundType = new Type[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)) {
|
||||||
|
foundType[0] = node.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundType[0] != null) {
|
||||||
|
return resolveTypeToFqn(foundType[0], receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check fields in enclosing class and its superclasses
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||||
|
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)) {
|
||||||
|
return resolveTypeToFqn(field.getType(), receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String superclass = context.getSuperclassFqn(currentType);
|
||||||
|
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
||||||
|
if (type == null) return null;
|
||||||
|
String simpleName;
|
||||||
|
if (type.isSimpleType()) {
|
||||||
|
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isParameterizedType()) {
|
||||||
|
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
|
||||||
|
} else {
|
||||||
|
simpleName = type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||||
|
if (td != null) {
|
||||||
|
return context.getFqn(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,93 @@
|
|||||||
|
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.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final Path rootDir;
|
||||||
|
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||||
|
private final GenericEventDetector eventDetector;
|
||||||
|
private final SpringMvcDetector mvcDetector;
|
||||||
|
private final MessagingDetector messagingDetector;
|
||||||
|
private final CallGraphEngine callGraphEngine;
|
||||||
|
private final LifecycleDetector lifecycleDetector;
|
||||||
|
private final InterceptorDetector interceptorDetector;
|
||||||
|
private final SpringComponentDetector componentDetector;
|
||||||
|
|
||||||
|
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||||
|
this.context = context;
|
||||||
|
this.rootDir = rootDir;
|
||||||
|
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
||||||
|
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
|
||||||
|
this.messagingDetector = new MessagingDetector(context);
|
||||||
|
|
||||||
|
SpringBeanRegistry registry = new SpringBeanRegistry();
|
||||||
|
SpringContextScanner scanner = new SpringContextScanner(registry);
|
||||||
|
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||||
|
cu.accept(scanner);
|
||||||
|
}
|
||||||
|
SpringDependencyResolver dependencyResolver = new SpringDependencyResolver(registry);
|
||||||
|
InjectionPointAnalyzer injectionAnalyzer = new InjectionPointAnalyzer(dependencyResolver);
|
||||||
|
|
||||||
|
this.callGraphEngine = new JdtCallGraphEngine(context, injectionAnalyzer);
|
||||||
|
|
||||||
|
this.lifecycleDetector = new LifecycleDetector(context);
|
||||||
|
this.interceptorDetector = new InterceptorDetector(context);
|
||||||
|
this.componentDetector = new SpringComponentDetector(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TriggerPoint> findTriggerPoints() {
|
||||||
|
List<TriggerPoint> allTriggers = new ArrayList<>();
|
||||||
|
var cus = context.getCompilationUnits();
|
||||||
|
log.info("JdtIntelligenceProvider scanning {} compilation units for triggers", cus.size());
|
||||||
|
for (CompilationUnit cu : cus) {
|
||||||
|
allTriggers.addAll(eventDetector.detect(cu));
|
||||||
|
allTriggers.addAll(lifecycleDetector.detect(cu));
|
||||||
|
}
|
||||||
|
log.info("Found {} triggers (including lifecycle) in total", allTriggers.size());
|
||||||
|
return allTriggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<EntryPoint> findEntryPoints() {
|
||||||
|
List<EntryPoint> allEntryPoints = new ArrayList<>();
|
||||||
|
var cus = context.getCompilationUnits();
|
||||||
|
log.info("JdtIntelligenceProvider scanning {} compilation units for entry points", cus.size());
|
||||||
|
for (CompilationUnit cu : cus) {
|
||||||
|
allEntryPoints.addAll(mvcDetector.detect(cu));
|
||||||
|
allEntryPoints.addAll(messagingDetector.detect(cu));
|
||||||
|
allEntryPoints.addAll(interceptorDetector.detect(cu));
|
||||||
|
allEntryPoints.addAll(componentDetector.detect(cu));
|
||||||
|
}
|
||||||
|
log.info("Found {} entry points (including interceptors and listeners) in total", allEntryPoints.size());
|
||||||
|
return allEntryPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
|
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
|
||||||
|
return callGraphEngine.findChains(entryPoints, triggers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Map<String, String>> resolveProperties() {
|
||||||
|
return context.getProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LifecycleDetector {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
public List<TriggerPoint> detect(CompilationUnit cu) {
|
||||||
|
List<TriggerPoint> lifecyclePoints = new ArrayList<>();
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
if ("restore".equals(methodName) || "reset".equals(methodName)) {
|
||||||
|
TriggerPoint point = buildLifecyclePoint(node, cu, methodName);
|
||||||
|
if (point != null) {
|
||||||
|
lifecyclePoints.add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lifecyclePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TriggerPoint buildLifecyclePoint(MethodInvocation node, CompilationUnit cu, String type) {
|
||||||
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
|
TypeDeclaration typeDecl = findEnclosingType(node);
|
||||||
|
|
||||||
|
if (typeDecl == null) return null;
|
||||||
|
|
||||||
|
return TriggerPoint.builder()
|
||||||
|
.event("[LIFECYCLE:" + type.toUpperCase() + "]")
|
||||||
|
.className(context.getFqn(typeDecl))
|
||||||
|
.methodName(method != null ? method.getName().getIdentifier() : "initializer")
|
||||||
|
.lineNumber(cu.getLineNumber(node.getStartPosition()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessagingDetector {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
public List<EntryPoint> detect(CompilationUnit cu) {
|
||||||
|
List<EntryPoint> entryPoints = new ArrayList<>();
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodDeclaration node) {
|
||||||
|
processMethod(node, entryPoints);
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return entryPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processMethod(MethodDeclaration method, List<EntryPoint> entryPoints) {
|
||||||
|
for (Object modifier : method.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation) {
|
||||||
|
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||||
|
|
||||||
|
EntryPoint.Type type = null;
|
||||||
|
String destination = "";
|
||||||
|
|
||||||
|
if (typeName.endsWith("JmsListener")) {
|
||||||
|
type = EntryPoint.Type.JMS;
|
||||||
|
destination = resolveValue(annotation, "destination");
|
||||||
|
} else if (typeName.endsWith("RabbitListener")) {
|
||||||
|
type = EntryPoint.Type.RABBIT;
|
||||||
|
destination = resolveValue(annotation, "queues");
|
||||||
|
} else if (typeName.endsWith("SqsListener")) {
|
||||||
|
type = EntryPoint.Type.SQS;
|
||||||
|
destination = resolveValue(annotation, "value");
|
||||||
|
if ("unknown".equals(destination)) {
|
||||||
|
destination = resolveValue(annotation, "queueNames");
|
||||||
|
}
|
||||||
|
} else if (typeName.endsWith("SnsListener")) {
|
||||||
|
type = EntryPoint.Type.SNS;
|
||||||
|
destination = resolveValue(annotation, "value");
|
||||||
|
if ("unknown".equals(destination)) {
|
||||||
|
destination = resolveValue(annotation, "topicNames");
|
||||||
|
}
|
||||||
|
} else if (typeName.endsWith("KafkaListener")) {
|
||||||
|
type = EntryPoint.Type.KAFKA;
|
||||||
|
destination = resolveValue(annotation, "topics");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
Map<String, String> metadata = new HashMap<>();
|
||||||
|
metadata.put("destination", destination);
|
||||||
|
|
||||||
|
String protocolName = type.name();
|
||||||
|
if (typeName.endsWith("SqsListener")) protocolName = "SQS";
|
||||||
|
if (typeName.endsWith("SnsListener")) protocolName = "SNS";
|
||||||
|
metadata.put("protocol", protocolName);
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(type)
|
||||||
|
.name(protocolName + ": " + destination)
|
||||||
|
.className(context.getFqn((TypeDeclaration) method.getParent()))
|
||||||
|
.methodName(method.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn((TypeDeclaration) method.getParent())))
|
||||||
|
.metadata(metadata)
|
||||||
|
.parameters(extractParameters(method))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveValue(Annotation annotation, String memberName) {
|
||||||
|
Expression value = null;
|
||||||
|
if (annotation instanceof SingleMemberAnnotation sma) {
|
||||||
|
value = sma.getValue();
|
||||||
|
} else if (annotation instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if (memberName.equals(pair.getName().getIdentifier())) {
|
||||||
|
value = pair.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
if (value instanceof ArrayInitializer ai) {
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
for (Object o : ai.expressions()) {
|
||||||
|
values.add(context.resolveExpression((Expression) o));
|
||||||
|
}
|
||||||
|
return String.join(", ", values);
|
||||||
|
}
|
||||||
|
return context.resolveExpression(value);
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EntryPoint.Parameter> extractParameters(MethodDeclaration method) {
|
||||||
|
List<EntryPoint.Parameter> parameters = new ArrayList<>();
|
||||||
|
for (Object paramObj : method.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration param) {
|
||||||
|
List<String> annotations = new ArrayList<>();
|
||||||
|
for (Object mod : param.modifiers()) {
|
||||||
|
if (mod instanceof Annotation ann) {
|
||||||
|
annotations.add(ann.getTypeName().getFullyQualifiedName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parameters.add(EntryPoint.Parameter.builder()
|
||||||
|
.name(param.getName().getIdentifier())
|
||||||
|
.type(param.getType().toString())
|
||||||
|
.annotations(annotations)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
|
import org.eclipse.jdt.core.dom.Annotation;
|
||||||
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
|
import org.eclipse.jdt.core.dom.MemberValuePair;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.NormalAnnotation;
|
||||||
|
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SpringComponentDetector {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
public List<EntryPoint> detect(CompilationUnit cu) {
|
||||||
|
List<EntryPoint> entryPoints = new ArrayList<>();
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodDeclaration node) {
|
||||||
|
if (!(node.getParent() instanceof TypeDeclaration)) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
TypeDeclaration parentTd = (TypeDeclaration) node.getParent();
|
||||||
|
|
||||||
|
for (Object modifier : node.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation) {
|
||||||
|
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||||
|
if (typeName.endsWith("Scheduled")) {
|
||||||
|
Map<String, String> meta = new HashMap<>();
|
||||||
|
String cron = extractAnnotationValue(annotation, "cron");
|
||||||
|
if (!cron.isEmpty())
|
||||||
|
meta.put("cron", cron);
|
||||||
|
String fixedRate = extractAnnotationValue(annotation, "fixedRate");
|
||||||
|
if (!fixedRate.isEmpty())
|
||||||
|
meta.put("fixedRate", fixedRate);
|
||||||
|
String fixedDelay = extractAnnotationValue(annotation, "fixedDelay");
|
||||||
|
if (!fixedDelay.isEmpty())
|
||||||
|
meta.put("fixedDelay", fixedDelay);
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.name("@Scheduled: " + node.getName().getIdentifier())
|
||||||
|
.className(context.getFqn(parentTd))
|
||||||
|
.methodName(node.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||||
|
.metadata(meta)
|
||||||
|
.build());
|
||||||
|
} else if (typeName.endsWith("EventListener")) {
|
||||||
|
Map<String, String> meta = new HashMap<>();
|
||||||
|
String classes = extractAnnotationValue(annotation, "classes");
|
||||||
|
if (!classes.isEmpty())
|
||||||
|
meta.put("classes", classes);
|
||||||
|
String condition = extractAnnotationValue(annotation, "condition");
|
||||||
|
if (!condition.isEmpty())
|
||||||
|
meta.put("condition", condition);
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.name("@EventListener: " + node.getName().getIdentifier())
|
||||||
|
.className(context.getFqn(parentTd))
|
||||||
|
.methodName(node.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||||
|
.metadata(meta)
|
||||||
|
.build());
|
||||||
|
} else if (typeName.endsWith("Around") || typeName.endsWith("Before") || typeName.endsWith("After") || typeName.endsWith("AfterReturning") || typeName.endsWith("AfterThrowing")) {
|
||||||
|
Map<String, String> meta = new HashMap<>();
|
||||||
|
String value = extractAnnotationValue(annotation, "value");
|
||||||
|
if (!value.isEmpty())
|
||||||
|
meta.put("pointcut", value);
|
||||||
|
else {
|
||||||
|
String pointcut = extractAnnotationValue(annotation, "pointcut");
|
||||||
|
if (!pointcut.isEmpty())
|
||||||
|
meta.put("pointcut", pointcut);
|
||||||
|
}
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.name("@" + annotation.getTypeName().getFullyQualifiedName() + ": " + node.getName().getIdentifier())
|
||||||
|
.className(context.getFqn(parentTd))
|
||||||
|
.methodName(node.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(parentTd)))
|
||||||
|
.metadata(meta)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return entryPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractAnnotationValue(Annotation annotation, String memberName) {
|
||||||
|
if (annotation instanceof SingleMemberAnnotation sma) {
|
||||||
|
if ("value".equals(memberName)) {
|
||||||
|
return sma.getValue().toString();
|
||||||
|
}
|
||||||
|
} else if (annotation instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if (pair.getName().getIdentifier().equals(memberName)) {
|
||||||
|
return pair.getValue().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SpringMvcDetector {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
|
||||||
|
public List<EntryPoint> detect(CompilationUnit cu) {
|
||||||
|
List<EntryPoint> entryPoints = new ArrayList<>();
|
||||||
|
cu.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(TypeDeclaration node) {
|
||||||
|
if (isRestController(node)) {
|
||||||
|
processController(node, entryPoints);
|
||||||
|
}
|
||||||
|
// Support WebFlux RouterFunction beans
|
||||||
|
processRouterFunctions(node, entryPoints);
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return entryPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processRouterFunctions(TypeDeclaration td, List<EntryPoint> entryPoints) {
|
||||||
|
for (MethodDeclaration method : td.getMethods()) {
|
||||||
|
if (isBeanMethod(method) && method.getReturnType2() != null && method.getReturnType2().toString().contains("RouterFunction")) {
|
||||||
|
method.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation call) {
|
||||||
|
String name = call.getName().getIdentifier();
|
||||||
|
String verb = getHttpVerb(name);
|
||||||
|
if (verb != null && !call.arguments().isEmpty()) {
|
||||||
|
Expression pathArg = (Expression) call.arguments().get(0);
|
||||||
|
String path = context.resolveExpression(pathArg);
|
||||||
|
|
||||||
|
entryPoints.add(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.REST)
|
||||||
|
.name(verb + " " + path)
|
||||||
|
.className(context.getFqn(td))
|
||||||
|
.methodName(method.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(td)))
|
||||||
|
.metadata(Map.of("path", path, "verb", verb, "style", "WebFlux Functional"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
return super.visit(call);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBeanMethod(MethodDeclaration method) {
|
||||||
|
for (Object mod : method.modifiers()) {
|
||||||
|
if (mod instanceof Annotation ann && ann.getTypeName().getFullyQualifiedName().endsWith("Bean")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRestController(TypeDeclaration node) {
|
||||||
|
for (Object modifier : node.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation) {
|
||||||
|
String name = annotation.getTypeName().getFullyQualifiedName();
|
||||||
|
if (name.endsWith("RestController") || name.endsWith("Controller") || name.endsWith("Path") || name.endsWith("RequestMapping")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processController(TypeDeclaration controller, List<EntryPoint> entryPoints) {
|
||||||
|
String basePath = getMappingPathHierarchical(controller);
|
||||||
|
if (basePath.isEmpty()) {
|
||||||
|
basePath = getJaxRsPath(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MethodDeclaration method : controller.getMethods()) {
|
||||||
|
EntryPoint ep = processMethodHierarchical(method, basePath, controller);
|
||||||
|
if (ep != null) {
|
||||||
|
entryPoints.add(ep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMappingPathHierarchical(TypeDeclaration td) {
|
||||||
|
String path = getMappingPath(td);
|
||||||
|
if (!path.isEmpty()) return path;
|
||||||
|
|
||||||
|
for (Object interfaceType : td.superInterfaceTypes()) {
|
||||||
|
TypeDeclaration itd = context.getTypeDeclaration(interfaceType.toString());
|
||||||
|
if (itd != null) {
|
||||||
|
path = getMappingPath(itd);
|
||||||
|
if (!path.isEmpty()) return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td.getSuperclassType() != null) {
|
||||||
|
TypeDeclaration std = context.getTypeDeclaration(td.getSuperclassType().toString());
|
||||||
|
if (std != null) {
|
||||||
|
return getMappingPathHierarchical(std);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private EntryPoint processMethodHierarchical(MethodDeclaration method, String basePath, TypeDeclaration controller) {
|
||||||
|
EntryPoint ep = processMethod(method, basePath, controller);
|
||||||
|
if (ep != null) return ep;
|
||||||
|
|
||||||
|
String methodName = method.getName().getIdentifier();
|
||||||
|
for (Object interfaceType : controller.superInterfaceTypes()) {
|
||||||
|
TypeDeclaration itd = context.getTypeDeclaration(interfaceType.toString());
|
||||||
|
if (itd != null) {
|
||||||
|
for (MethodDeclaration im : itd.getMethods()) {
|
||||||
|
if (im.getName().getIdentifier().equals(methodName)) {
|
||||||
|
ep = processMethod(im, basePath, controller);
|
||||||
|
if (ep != null) return ep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EntryPoint processMethod(MethodDeclaration method, String basePath, TypeDeclaration controller) {
|
||||||
|
for (Object modifier : method.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation) {
|
||||||
|
String name = annotation.getTypeName().getFullyQualifiedName();
|
||||||
|
String verb = getHttpVerb(name);
|
||||||
|
if (verb != null) {
|
||||||
|
String methodPath = getMappingPath(annotation);
|
||||||
|
if (methodPath.isEmpty()) {
|
||||||
|
methodPath = getJaxRsPath(method);
|
||||||
|
}
|
||||||
|
String fullPath = combinePaths(basePath, methodPath);
|
||||||
|
|
||||||
|
Map<String, String> metadata = new HashMap<>();
|
||||||
|
metadata.put("path", fullPath);
|
||||||
|
metadata.put("verb", verb);
|
||||||
|
|
||||||
|
List<EntryPoint.Parameter> parameters = extractParameters(method);
|
||||||
|
|
||||||
|
return EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.REST)
|
||||||
|
.name(verb + " " + fullPath)
|
||||||
|
.className(context.getFqn(controller))
|
||||||
|
.methodName(method.getName().getIdentifier())
|
||||||
|
.sourceFile(context.getRelativePath(context.getFqn(controller)))
|
||||||
|
.metadata(metadata)
|
||||||
|
.parameters(parameters)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getJaxRsPath(BodyDeclaration node) {
|
||||||
|
for (Object mod : node.modifiers()) {
|
||||||
|
if (mod instanceof Annotation ann && ann.getTypeName().getFullyQualifiedName().endsWith("Path")) {
|
||||||
|
return getMappingPath(ann);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getHttpVerb(String annotationName) {
|
||||||
|
if (annotationName.endsWith("PostMapping") || annotationName.endsWith("POST")) return "POST";
|
||||||
|
if (annotationName.endsWith("GetMapping") || annotationName.endsWith("GET")) return "GET";
|
||||||
|
if (annotationName.endsWith("PutMapping") || annotationName.endsWith("PUT")) return "PUT";
|
||||||
|
if (annotationName.endsWith("DeleteMapping") || annotationName.endsWith("DELETE")) return "DELETE";
|
||||||
|
if (annotationName.endsWith("PatchMapping") || annotationName.endsWith("PATCH")) return "PATCH";
|
||||||
|
if (annotationName.endsWith("RequestMapping")) return "REQUEST";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMappingPath(TypeDeclaration td) {
|
||||||
|
for (Object modifier : td.modifiers()) {
|
||||||
|
if (modifier instanceof Annotation annotation) {
|
||||||
|
String name = annotation.getTypeName().getFullyQualifiedName();
|
||||||
|
if (name.endsWith("RequestMapping") || name.endsWith("Path")) {
|
||||||
|
return getMappingPath(annotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMappingPath(Annotation annotation) {
|
||||||
|
Expression value = null;
|
||||||
|
if (annotation instanceof SingleMemberAnnotation sma) {
|
||||||
|
value = sma.getValue();
|
||||||
|
} else if (annotation instanceof NormalAnnotation na) {
|
||||||
|
for (Object pairObj : na.values()) {
|
||||||
|
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||||
|
if ("value".equals(pair.getName().getIdentifier()) || "path".equals(pair.getName().getIdentifier())) {
|
||||||
|
value = pair.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return context.resolveExpression(value);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EntryPoint.Parameter> extractParameters(MethodDeclaration method) {
|
||||||
|
List<EntryPoint.Parameter> parameters = new ArrayList<>();
|
||||||
|
for (Object paramObj : method.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration param) {
|
||||||
|
List<String> annotations = new ArrayList<>();
|
||||||
|
for (Object mod : param.modifiers()) {
|
||||||
|
if (mod instanceof Annotation ann) {
|
||||||
|
String name = ann.getTypeName().getFullyQualifiedName();
|
||||||
|
if (name.endsWith("PathParam")) name = "PathVariable";
|
||||||
|
if (name.endsWith("QueryParam")) name = "RequestParam";
|
||||||
|
annotations.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parameters.add(EntryPoint.Parameter.builder()
|
||||||
|
.name(param.getName().getIdentifier())
|
||||||
|
.type(param.getType().toString())
|
||||||
|
.annotations(annotations)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String combinePaths(String base, String method) {
|
||||||
|
String b = base.startsWith("/") ? base : "/" + base;
|
||||||
|
if (b.endsWith("/")) b = b.substring(0, b.length() - 1);
|
||||||
|
String m = method.startsWith("/") ? method : "/" + method;
|
||||||
|
if ("/".equals(m)) m = "";
|
||||||
|
return b + m;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,597 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class VariableTracer {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
private ConstantExtractor constantExtractor;
|
||||||
|
|
||||||
|
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
|
||||||
|
|
||||||
|
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
|
||||||
|
this.context = context;
|
||||||
|
this.constantResolver = constantResolver;
|
||||||
|
this.dataFlowModel = new click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstantExtractor(ConstantExtractor constantExtractor) {
|
||||||
|
this.constantExtractor = constantExtractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Expression traceVariable(Expression expr) {
|
||||||
|
List<Expression> all = traceVariableAll(expr);
|
||||||
|
return all.isEmpty() ? expr : all.get(all.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Expression> traceVariableAll(Expression expr) {
|
||||||
|
return dataFlowModel.getReachingDefinitions(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
|
||||||
|
return dataFlowModel.getReachingDefinitions(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Expression traceLocalSetter(String methodFqn, String varName, String getterName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
final Expression[] setterArg = new Expression[1];
|
||||||
|
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (node.getName().getIdentifier().equalsIgnoreCase("set" + propName) || node.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
if (node.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
|
||||||
|
if (!node.arguments().isEmpty()) {
|
||||||
|
setterArg[0] = (Expression) node.arguments().get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof FieldAccess fa) {
|
||||||
|
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
||||||
|
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
setterArg[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.getLeftHandSide() instanceof QualifiedName qqn) {
|
||||||
|
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
||||||
|
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
||||||
|
setterArg[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return setterArg[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
|
||||||
|
if (methodFqn == null) return null;
|
||||||
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) {
|
||||||
|
String fqn = methodFqn.substring(0, lastDot);
|
||||||
|
String methodName = methodFqn.substring(lastDot + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(fqn);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
|
||||||
|
if (md != null) {
|
||||||
|
for (Object pObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(cleanFieldName)) {
|
||||||
|
return svd.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String[] foundType = new String[1];
|
||||||
|
if (md.getBody() != null) {
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragObj : node.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
|
||||||
|
foundType[0] = node.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(LambdaExpression node) {
|
||||||
|
for (Object paramObj : node.parameters()) {
|
||||||
|
VariableDeclaration param = (VariableDeclaration) paramObj;
|
||||||
|
if (param.getName().getIdentifier().equals(cleanFieldName)) {
|
||||||
|
if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
|
||||||
|
foundType[0] = svd.getType().toString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
if (parent instanceof MethodInvocation mi) {
|
||||||
|
Expression caller = mi.getExpression();
|
||||||
|
|
||||||
|
boolean hasMap = false;
|
||||||
|
while (caller instanceof MethodInvocation chainMi) {
|
||||||
|
String mName = chainMi.getName().getIdentifier();
|
||||||
|
if (mName.equals("map") || mName.equals("flatMap")) {
|
||||||
|
hasMap = true;
|
||||||
|
if (chainMi.arguments().size() == 1 && chainMi.arguments().get(0) instanceof LambdaExpression lambda) {
|
||||||
|
if (lambda.getBody() instanceof MethodInvocation mapMi) {
|
||||||
|
Expression mapCaller = mapMi.getExpression();
|
||||||
|
if (mapCaller instanceof SimpleName mapSn) {
|
||||||
|
String mapCallerType = getVariableDeclaredType(methodFqn, mapSn.getIdentifier());
|
||||||
|
if (mapCallerType != null) {
|
||||||
|
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||||
|
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(mapCallerType, cu);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration mapMd = context.findMethodDeclaration(td, mapMi.getName().getIdentifier(), true);
|
||||||
|
if (mapMd != null && mapMd.getReturnType2() != null) {
|
||||||
|
foundType[0] = mapMd.getReturnType2().toString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mName.equals("stream") || mName.equals("filter") || mName.equals("map") ||
|
||||||
|
mName.equals("flatMap") || mName.equals("peek") || mName.equals("distinct") ||
|
||||||
|
mName.equals("sorted") || mName.equals("limit") || mName.equals("skip")) {
|
||||||
|
caller = chainMi.getExpression();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMap) {
|
||||||
|
if (caller instanceof SimpleName callerSn) {
|
||||||
|
String callerName = callerSn.getIdentifier();
|
||||||
|
if (!callerName.equals(cleanFieldName)) {
|
||||||
|
String callerType = getVariableDeclaredType(methodFqn, callerName);
|
||||||
|
if (callerType != null && callerType.contains("<")) {
|
||||||
|
int start = callerType.indexOf('<');
|
||||||
|
int end = callerType.lastIndexOf('>');
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
foundType[0] = callerType.substring(start + 1, end);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (caller instanceof FieldAccess fa) {
|
||||||
|
String callerName = fa.getName().getIdentifier();
|
||||||
|
if (!callerName.equals(cleanFieldName)) {
|
||||||
|
String callerType = getVariableDeclaredType(methodFqn, callerName);
|
||||||
|
if (callerType != null && callerType.contains("<")) {
|
||||||
|
int start = callerType.indexOf('<');
|
||||||
|
int end = callerType.lastIndexOf('>');
|
||||||
|
if (start >= 0 && end > start) {
|
||||||
|
foundType[0] = callerType.substring(start + 1, end);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (foundType[0] != null) return foundType[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to fields
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
|
||||||
|
return fd.getType().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback: ask constantExtractor to resolve it via method return constant
|
||||||
|
if (constantExtractor != null && methodFqn.contains(".")) {
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
CompilationUnit cu = td != null && td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
List<String> values = constantExtractor.resolveMethodReturnConstant(cleanFieldName, methodName, 0, new HashSet<>(), cu);
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
// If it resolves to some class constant, we could infer type or return a fallback
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String traceLocalVariable(String methodFqn, String varName) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
List<Expression> initializers = new ArrayList<>();
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
initializers.add(node.getInitializer());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
initializers.add(node.getRightHandSide());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!initializers.isEmpty()) {
|
||||||
|
List<String> stringified = new ArrayList<>();
|
||||||
|
for (Expression expr : initializers) {
|
||||||
|
Expression traced = traceVariable(expr);
|
||||||
|
if (traced instanceof MethodInvocation mi) {
|
||||||
|
Expression innerMost = unwrapMethodInvocation(mi, 0, methodFqn);
|
||||||
|
stringified.add(innerMost.toString());
|
||||||
|
} else if (traced instanceof ArrayInitializer) {
|
||||||
|
stringified.add("new Object[]" + traced.toString());
|
||||||
|
} else {
|
||||||
|
stringified.add(traced.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stringified.size() == 1) return stringified.get(0);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < stringified.size() - 1; i++) {
|
||||||
|
sb.append("true ? ").append(stringified.get(i)).append(" : ");
|
||||||
|
}
|
||||||
|
sb.append(stringified.get(stringified.size() - 1));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If local variable not found, check field assignments
|
||||||
|
String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName;
|
||||||
|
final Expression[] fieldInitializer = new Expression[1];
|
||||||
|
ASTVisitor fieldVisitor = new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) {
|
||||||
|
fieldInitializer[0] = node.getInitializer();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression left = node.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) ||
|
||||||
|
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) {
|
||||||
|
fieldInitializer[0] = node.getRightHandSide();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (FieldDeclaration fd : td.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||||
|
if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) {
|
||||||
|
fieldInitializer[0] = frag.getInitializer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||||
|
|
||||||
|
for (MethodDeclaration classMd : td.getMethods()) {
|
||||||
|
if (classMd.isConstructor() && classMd.getBody() != null) {
|
||||||
|
classMd.getBody().accept(fieldVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||||
|
|
||||||
|
for (MethodDeclaration classMd : td.getMethods()) {
|
||||||
|
if (!classMd.isConstructor() && classMd.getBody() != null) {
|
||||||
|
classMd.getBody().accept(fieldVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String traceLocalVariable(String methodFqn, String varName, Map<String, String> parameterValues) {
|
||||||
|
String result = traceLocalVariable(methodFqn, varName);
|
||||||
|
if (result != null && parameterValues != null && !parameterValues.isEmpty()) {
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||||
|
if (td != null) {
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md != null && md.getBody() != null) {
|
||||||
|
final ASTNode[] switchNode = new ASTNode[1];
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
Expression init = node.getInitializer();
|
||||||
|
if (init.getNodeType() == ASTNode.SWITCH_EXPRESSION) {
|
||||||
|
switchNode[0] = init;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (init.getNodeType() == ASTNode.SWITCH_STATEMENT) {
|
||||||
|
switchNode[0] = init;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||||
|
Expression rhs = node.getRightHandSide();
|
||||||
|
if (rhs.getNodeType() == ASTNode.SWITCH_EXPRESSION) {
|
||||||
|
switchNode[0] = rhs;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rhs.getNodeType() == ASTNode.SWITCH_STATEMENT) {
|
||||||
|
switchNode[0] = rhs;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (switchNode[0] != null) {
|
||||||
|
if (switchNode[0] instanceof SwitchExpression se) {
|
||||||
|
String evaluated = constantResolver.evaluateSwitchWithParams(se, parameterValues, context);
|
||||||
|
if (evaluated != null) return evaluated;
|
||||||
|
} else if (switchNode[0] instanceof SwitchStatement ss) {
|
||||||
|
String evaluated = constantResolver.evaluateSwitchWithParams(ss, parameterValues, context);
|
||||||
|
if (evaluated != null) return evaluated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildParameterValuesMap(String caller, String target, Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
|
||||||
|
Map<String, String> paramValues = new HashMap<>();
|
||||||
|
List<CallEdge> edges = callGraph.get(caller);
|
||||||
|
if (edges == null) {
|
||||||
|
return paramValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also want to support heuristic matches using a helper if isHeuristicMatch is not directly available, but let's check
|
||||||
|
// We can check if name equals or if it resolves to target
|
||||||
|
for (CallEdge edge : edges) {
|
||||||
|
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||||
|
List<String> args = edge.getArguments();
|
||||||
|
if (args == null || args.isEmpty()) break;
|
||||||
|
|
||||||
|
int lastDot = target.lastIndexOf('.');
|
||||||
|
if (lastDot < 0) break;
|
||||||
|
String className = target.substring(0, lastDot);
|
||||||
|
String methodName = target.substring(lastDot + 1);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) break;
|
||||||
|
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md == null) break;
|
||||||
|
|
||||||
|
List<String> paramNames = new ArrayList<>();
|
||||||
|
for (Object paramObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
||||||
|
paramNames.add(param.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
int minSize = Math.min(paramNames.size(), args.size());
|
||||||
|
for (int j = 0; j < minSize; j++) {
|
||||||
|
String argValue = args.get(j);
|
||||||
|
String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph);
|
||||||
|
paramValues.put(paramNames.get(j), resolvedArgValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paramValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveArgumentValue(String argValue, String callerMethod, List<String> path, int pathIndex, Map<String, List<CallEdge>> callGraph) {
|
||||||
|
if (argValue == null) return null;
|
||||||
|
if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*")) {
|
||||||
|
return argValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int callerIndex = path.indexOf(callerMethod);
|
||||||
|
if (callerIndex <= 0) return argValue;
|
||||||
|
|
||||||
|
String prevCaller = path.get(callerIndex - 1);
|
||||||
|
List<CallEdge> prevEdges = callGraph.get(prevCaller);
|
||||||
|
if (prevEdges == null) return argValue;
|
||||||
|
|
||||||
|
for (CallEdge edge : prevEdges) {
|
||||||
|
if (edge.getTargetMethod().equals(callerMethod) || isHeuristicMatch(edge.getTargetMethod(), callerMethod)) {
|
||||||
|
List<String> prevArgs = edge.getArguments();
|
||||||
|
if (prevArgs == null || prevArgs.isEmpty()) break;
|
||||||
|
|
||||||
|
int lastDot = callerMethod.lastIndexOf('.');
|
||||||
|
if (lastDot < 0) break;
|
||||||
|
String className = callerMethod.substring(0, lastDot);
|
||||||
|
String methodName = callerMethod.substring(lastDot + 1);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) break;
|
||||||
|
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md == null) break;
|
||||||
|
|
||||||
|
List<String> paramNames = new ArrayList<>();
|
||||||
|
for (Object paramObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
||||||
|
paramNames.add(param.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
int paramIdx = paramNames.indexOf(argValue);
|
||||||
|
if (paramIdx >= 0 && paramIdx < prevArgs.size()) {
|
||||||
|
String prevArg = prevArgs.get(paramIdx);
|
||||||
|
return resolveArgumentValue(prevArg, prevCaller, path, pathIndex, callGraph);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return argValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||||
|
if (neighbor == null || target == null) return false;
|
||||||
|
if (neighbor.equals(target)) return true;
|
||||||
|
|
||||||
|
if (neighbor.contains(".") && target.contains(".")) {
|
||||||
|
String methodNeighbor = neighbor.substring(neighbor.lastIndexOf('.') + 1);
|
||||||
|
String methodTarget = target.substring(target.lastIndexOf('.') + 1);
|
||||||
|
if (!methodNeighbor.equals(methodTarget)) return false;
|
||||||
|
|
||||||
|
String classNeighbor = neighbor.substring(0, neighbor.lastIndexOf('.'));
|
||||||
|
String classTarget = target.substring(0, target.lastIndexOf('.'));
|
||||||
|
if (classNeighbor.equals(classTarget)) return true;
|
||||||
|
|
||||||
|
String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
|
||||||
|
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
|
||||||
|
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
|
||||||
|
|
||||||
|
List<String> impls = context.getImplementations(classTarget);
|
||||||
|
if (impls != null && impls.contains(classNeighbor)) return true;
|
||||||
|
|
||||||
|
List<String> implsN = context.getImplementations(classNeighbor);
|
||||||
|
if (implsN != null && implsN.contains(classTarget)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (MethodDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode parent = node.getParent();
|
||||||
|
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return (TypeDeclaration) parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getReturnedParameterIndex(MethodDeclaration md) {
|
||||||
|
if (md.getBody() == null) return -1;
|
||||||
|
List<?> parameters = md.parameters();
|
||||||
|
if (parameters.isEmpty()) return -1;
|
||||||
|
|
||||||
|
final List<String> returnedExprs = new ArrayList<>();
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement node) {
|
||||||
|
if (node.getExpression() != null) {
|
||||||
|
returnedExprs.add(node.getExpression().toString());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (returnedExprs.size() == 1) {
|
||||||
|
String retStr = returnedExprs.get(0);
|
||||||
|
for (int i = 0; i < parameters.size(); i++) {
|
||||||
|
if (parameters.get(i) instanceof SingleVariableDeclaration svd) {
|
||||||
|
if (svd.getName().getIdentifier().equals(retStr)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
||||||
|
if (depth > 5) return mi;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (methodFqn != null && methodFqn.contains(".")) {
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td != null) {
|
||||||
|
MethodDeclaration localMd = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||||
|
if (localMd != null) {
|
||||||
|
int paramIdx = getReturnedParameterIndex(localMd);
|
||||||
|
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(paramIdx);
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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")) {
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
if (arg instanceof MethodInvocation innerMi) {
|
||||||
|
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
return mi;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.IAnnotationBinding;
|
||||||
|
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
|
||||||
|
import org.eclipse.jdt.core.dom.IVariableBinding;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class InjectionPointAnalyzer {
|
||||||
|
|
||||||
|
private final SpringDependencyResolver resolver;
|
||||||
|
|
||||||
|
public InjectionPointAnalyzer(SpringDependencyResolver resolver) {
|
||||||
|
this.resolver = resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the Spring bean injected into the given variable.
|
||||||
|
*
|
||||||
|
* @param variableBinding The binding of the field or parameter being injected.
|
||||||
|
* @return The FQN of the resolved concrete bean class, or null if unresolved.
|
||||||
|
*/
|
||||||
|
public String resolveInjectedBeanFqn(IVariableBinding variableBinding) {
|
||||||
|
if (variableBinding == null || variableBinding.getType() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String requiredTypeFqn = variableBinding.getType().getQualifiedName();
|
||||||
|
String injectionName = variableBinding.getName();
|
||||||
|
String qualifier = extractQualifier(variableBinding);
|
||||||
|
|
||||||
|
List<SpringBean> resolvedBeans = resolver.resolve(requiredTypeFqn, qualifier, injectionName);
|
||||||
|
|
||||||
|
if (resolvedBeans != null && resolvedBeans.size() == 1) {
|
||||||
|
return resolvedBeans.get(0).getTypeFqn();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractQualifier(IVariableBinding binding) {
|
||||||
|
String qualifier = getQualifierValue(binding.getAnnotations());
|
||||||
|
if (qualifier != null) {
|
||||||
|
return qualifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.isField()) {
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
for (org.eclipse.jdt.core.dom.IMethodBinding method : declaringClass.getDeclaredMethods()) {
|
||||||
|
boolean isAutowiredMethod = false;
|
||||||
|
for (IAnnotationBinding ann : method.getAnnotations()) {
|
||||||
|
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Autowired".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||||
|
isAutowiredMethod = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method.isConstructor() || isAutowiredMethod) {
|
||||||
|
for (int i = 0; i < method.getParameterTypes().length; i++) {
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding paramType = method.getParameterTypes()[i];
|
||||||
|
try {
|
||||||
|
IAnnotationBinding[] paramAnns = method.getParameterAnnotations(i);
|
||||||
|
String paramQual = getQualifierValue(paramAnns);
|
||||||
|
if (paramQual != null && paramType.getErasure().isEqualTo(binding.getType().getErasure())) {
|
||||||
|
// For setters, verify it roughly matches the field name or just rely on type.
|
||||||
|
// We will rely on type equality for now as a heuristic.
|
||||||
|
return paramQual;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getQualifierValue(IAnnotationBinding[] annotations) {
|
||||||
|
for (IAnnotationBinding ann : annotations) {
|
||||||
|
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||||
|
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||||
|
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
|
||||||
|
return (String) pair.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class SpringBean {
|
||||||
|
private String typeFqn;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Set<String> assignableTypes = new HashSet<>();
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Set<String> beanNames = new HashSet<>();
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Set<String> qualifiers = new HashSet<>();
|
||||||
|
|
||||||
|
private boolean isPrimary;
|
||||||
|
private Integer order;
|
||||||
|
private String declaringClassFqn;
|
||||||
|
private String factoryMethodName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SpringBeanRegistry {
|
||||||
|
private final List<SpringBean> beans = new ArrayList<>();
|
||||||
|
|
||||||
|
public void addBean(SpringBean bean) {
|
||||||
|
beans.add(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SpringBean> getBeans() {
|
||||||
|
return Collections.unmodifiableList(beans);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class SpringContextScanner extends ASTVisitor {
|
||||||
|
private final SpringBeanRegistry registry;
|
||||||
|
|
||||||
|
public SpringContextScanner(SpringBeanRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(TypeDeclaration node) {
|
||||||
|
ITypeBinding typeBinding = node.resolveBinding();
|
||||||
|
if (typeBinding == null) return true;
|
||||||
|
|
||||||
|
if (isSpringComponent(typeBinding)) {
|
||||||
|
SpringBean bean = SpringBean.builder()
|
||||||
|
.typeFqn(typeBinding.getQualifiedName())
|
||||||
|
.assignableTypes(getAssignableTypes(typeBinding))
|
||||||
|
.isPrimary(hasAnnotation(typeBinding, "org.springframework.context.annotation.Primary"))
|
||||||
|
.qualifiers(extractQualifiers(typeBinding))
|
||||||
|
.order(extractOrder(typeBinding))
|
||||||
|
.declaringClassFqn(typeBinding.getQualifiedName())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Default bean name is uncapitalized simple name
|
||||||
|
String defaultName = Character.toLowerCase(node.getName().getIdentifier().charAt(0)) + node.getName().getIdentifier().substring(1);
|
||||||
|
|
||||||
|
// Check if @Component or @Service specifies a name (e.g., @Service("myService"))
|
||||||
|
String explicitName = extractStereotypeValue(typeBinding);
|
||||||
|
if (explicitName != null && !explicitName.isEmpty()) {
|
||||||
|
bean.getBeanNames().add(explicitName);
|
||||||
|
} else {
|
||||||
|
bean.getBeanNames().add(defaultName);
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.addBean(bean);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodDeclaration node) {
|
||||||
|
IMethodBinding methodBinding = node.resolveBinding();
|
||||||
|
if (methodBinding == null) return true;
|
||||||
|
|
||||||
|
if (hasAnnotation(methodBinding, "org.springframework.context.annotation.Bean")) {
|
||||||
|
ITypeBinding returnType = methodBinding.getReturnType();
|
||||||
|
if (returnType != null) {
|
||||||
|
Set<String> assignables = getAssignableTypes(returnType);
|
||||||
|
String[] concreteType = new String[] { returnType.getQualifiedName() };
|
||||||
|
|
||||||
|
if (node.getBody() != null) {
|
||||||
|
node.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement ret) {
|
||||||
|
if (ret.getExpression() != null) {
|
||||||
|
ITypeBinding exprType = ret.getExpression().resolveTypeBinding();
|
||||||
|
if (exprType != null && exprType.isClass()) {
|
||||||
|
concreteType[0] = exprType.getQualifiedName();
|
||||||
|
assignables.addAll(getAssignableTypes(exprType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(ret);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SpringBean bean = SpringBean.builder()
|
||||||
|
.typeFqn(concreteType[0])
|
||||||
|
.assignableTypes(assignables)
|
||||||
|
.isPrimary(hasAnnotation(methodBinding, "org.springframework.context.annotation.Primary"))
|
||||||
|
.qualifiers(extractQualifiers(methodBinding))
|
||||||
|
.order(extractOrder(methodBinding))
|
||||||
|
.declaringClassFqn(methodBinding.getDeclaringClass().getQualifiedName())
|
||||||
|
.factoryMethodName(node.getName().getIdentifier())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Default bean name is method name
|
||||||
|
String defaultName = node.getName().getIdentifier();
|
||||||
|
|
||||||
|
// Check if @Bean specifies a name (e.g., @Bean("myBean"))
|
||||||
|
String explicitName = extractBeanName(methodBinding);
|
||||||
|
if (explicitName != null && !explicitName.isEmpty()) {
|
||||||
|
bean.getBeanNames().add(explicitName);
|
||||||
|
} else {
|
||||||
|
bean.getBeanNames().add(defaultName);
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.addBean(bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSpringComponent(ITypeBinding binding) {
|
||||||
|
return hasMetaAnnotation(binding, "org.springframework.stereotype.Component") ||
|
||||||
|
hasMetaAnnotation(binding, "org.springframework.web.bind.annotation.RestController");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasMetaAnnotation(ITypeBinding binding, String targetFqn) {
|
||||||
|
return checkMetaAnnotation(binding.getAnnotations(), targetFqn, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkMetaAnnotation(IAnnotationBinding[] annotations, String targetFqn, Set<String> visited) {
|
||||||
|
for (IAnnotationBinding ann : annotations) {
|
||||||
|
ITypeBinding annType = ann.getAnnotationType();
|
||||||
|
if (annType != null) {
|
||||||
|
String fqn = annType.getQualifiedName();
|
||||||
|
if (fqn.equals(targetFqn)) return true;
|
||||||
|
|
||||||
|
// Avoid circular meta-annotations (e.g. Documented -> Documented)
|
||||||
|
if (visited.add(fqn)) {
|
||||||
|
if (checkMetaAnnotation(annType.getAnnotations(), targetFqn, visited)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasAnnotation(IBinding binding, String annotationFqn) {
|
||||||
|
IAnnotationBinding[] annotations = binding.getAnnotations();
|
||||||
|
for (IAnnotationBinding ann : annotations) {
|
||||||
|
if (ann.getAnnotationType() != null && annotationFqn.equals(ann.getAnnotationType().getQualifiedName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getAssignableTypes(ITypeBinding binding) {
|
||||||
|
Set<String> types = new HashSet<>();
|
||||||
|
collectAssignableTypes(binding, types);
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectAssignableTypes(ITypeBinding binding, Set<String> types) {
|
||||||
|
if (binding == null) return;
|
||||||
|
|
||||||
|
String qName = binding.getQualifiedName();
|
||||||
|
if (qName != null && !qName.isEmpty()) {
|
||||||
|
types.add(qName);
|
||||||
|
}
|
||||||
|
|
||||||
|
ITypeBinding erasure = binding.getErasure();
|
||||||
|
if (erasure != null) {
|
||||||
|
String erasureName = erasure.getQualifiedName();
|
||||||
|
if (erasureName != null && !erasureName.isEmpty()) {
|
||||||
|
types.add(erasureName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectAssignableTypes(binding.getSuperclass(), types);
|
||||||
|
if (binding.getInterfaces() != null) {
|
||||||
|
for (ITypeBinding iface : binding.getInterfaces()) {
|
||||||
|
collectAssignableTypes(iface, types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> extractQualifiers(IBinding binding) {
|
||||||
|
Set<String> qualifiers = new HashSet<>();
|
||||||
|
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
||||||
|
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||||
|
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||||
|
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
|
||||||
|
qualifiers.add((String) pair.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return qualifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer extractOrder(IBinding binding) {
|
||||||
|
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
||||||
|
if (ann.getAnnotationType() != null && "org.springframework.core.annotation.Order".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||||
|
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||||
|
if ("value".equals(pair.getName())) {
|
||||||
|
Object val = pair.getValue();
|
||||||
|
if (val instanceof Integer) {
|
||||||
|
return (Integer) val;
|
||||||
|
} else if (val instanceof IVariableBinding) {
|
||||||
|
Object constantValue = ((IVariableBinding) val).getConstantValue();
|
||||||
|
if (constantValue instanceof Integer) {
|
||||||
|
return (Integer) constantValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Integer.MAX_VALUE; // Spring's default for @Order()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractStereotypeValue(ITypeBinding binding) {
|
||||||
|
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
||||||
|
// Check if annotation itself is meta-annotated with @Component
|
||||||
|
if (checkMetaAnnotation(new IAnnotationBinding[]{ann}, "org.springframework.stereotype.Component", new HashSet<>()) ||
|
||||||
|
"org.springframework.stereotype.Component".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||||
|
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||||
|
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
|
||||||
|
return (String) pair.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractBeanName(IMethodBinding binding) {
|
||||||
|
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
||||||
|
if ("org.springframework.context.annotation.Bean".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||||
|
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||||
|
if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof String) {
|
||||||
|
return (String) pair.getValue();
|
||||||
|
} else if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof Object[]) {
|
||||||
|
Object[] vals = (Object[]) pair.getValue();
|
||||||
|
if (vals.length > 0 && vals[0] instanceof String) {
|
||||||
|
return (String) vals[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class SpringDependencyResolver {
|
||||||
|
private final SpringBeanRegistry registry;
|
||||||
|
|
||||||
|
public SpringDependencyResolver(SpringBeanRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the exact SpringBean that would be injected for a given injection point.
|
||||||
|
*
|
||||||
|
* @param requiredTypeFqn The exact FQN of the type requested at the injection point.
|
||||||
|
* @param qualifier The value of the @Qualifier annotation on the injection point (if any).
|
||||||
|
* @param injectionName The name of the field or parameter being injected (used as fallback).
|
||||||
|
* @return A list of resolved beans. Usually 1. If 0, unresolved. If > 1, ambiguous.
|
||||||
|
*/
|
||||||
|
public List<SpringBean> resolve(String requiredTypeFqn, String qualifier, String injectionName) {
|
||||||
|
if (requiredTypeFqn == null) return new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. Filter by Type (Exact FQN or Assignable Type)
|
||||||
|
System.out.println("RESOLVING " + requiredTypeFqn + " qual=" + qualifier + " injectionName=" + injectionName);
|
||||||
|
List<SpringBean> candidates = registry.getBeans().stream()
|
||||||
|
.filter(bean -> requiredTypeFqn.equals(bean.getTypeFqn()) || bean.getAssignableTypes().contains(requiredTypeFqn))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
System.out.println("CANDIDATES AFTER TYPE: " + candidates.stream().map(c -> c.getTypeFqn() + " (primary=" + c.isPrimary() + " names=" + c.getBeanNames() + " qual=" + c.getQualifiers() + ")").collect(Collectors.toList()));
|
||||||
|
if (candidates.isEmpty() || candidates.size() == 1) {
|
||||||
|
System.out.println("RETURNING: " + candidates.size());
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Filter by Qualifier (if provided)
|
||||||
|
if (qualifier != null && !qualifier.isEmpty()) {
|
||||||
|
List<SpringBean> qualifiedCandidates = candidates.stream()
|
||||||
|
.filter(bean -> bean.getQualifiers().contains(qualifier) || bean.getBeanNames().contains(qualifier))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (qualifiedCandidates.size() == 1) {
|
||||||
|
return qualifiedCandidates;
|
||||||
|
} else if (!qualifiedCandidates.isEmpty()) {
|
||||||
|
candidates = qualifiedCandidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Filter by @Primary
|
||||||
|
List<SpringBean> primaryCandidates = candidates.stream()
|
||||||
|
.filter(SpringBean::isPrimary)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (primaryCandidates.size() == 1) {
|
||||||
|
return primaryCandidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback to Injection Name (field name or parameter name)
|
||||||
|
if (injectionName != null && !injectionName.isEmpty()) {
|
||||||
|
List<SpringBean> namedCandidates = candidates.stream()
|
||||||
|
.filter(bean -> bean.getBeanNames().contains(injectionName))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (namedCandidates.size() == 1) {
|
||||||
|
return namedCandidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Fallback to @Order (lowest order wins)
|
||||||
|
Integer minOrder = candidates.stream()
|
||||||
|
.map(SpringBean::getOrder)
|
||||||
|
.filter(o -> o != null)
|
||||||
|
.min(Integer::compareTo)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (minOrder != null) {
|
||||||
|
List<SpringBean> orderedCandidates = candidates.stream()
|
||||||
|
.filter(bean -> bean.getOrder() != null && bean.getOrder().equals(minOrder))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!orderedCandidates.isEmpty()) {
|
||||||
|
candidates = orderedCandidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return whatever candidates remain (could be ambiguous)
|
||||||
|
System.out.println("RETURNING: " + candidates.size());
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.util;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for resolving Spring-style placeholders like ${property.name} or ${property.name:default-value}.
|
||||||
|
* Supports recursive resolution.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class PlaceholderResolver {
|
||||||
|
|
||||||
|
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^:}]+)(?::([^}]*))?}");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves all placeholders in the given text using the provided properties map.
|
||||||
|
*
|
||||||
|
* @param text The text containing placeholders.
|
||||||
|
* @param properties The map of property keys and values.
|
||||||
|
* @return The resolved string.
|
||||||
|
*/
|
||||||
|
public static String resolve(String text, Map<String, String> properties) {
|
||||||
|
return resolveInternal(text, properties, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveInternal(String text, Map<String, String> properties, Set<String> visited) {
|
||||||
|
if (text == null || !text.contains("${")) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
|
||||||
|
int lastMatchEnd = 0;
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
result.append(text, lastMatchEnd, matcher.start());
|
||||||
|
String key = matcher.group(1);
|
||||||
|
String defaultValue = matcher.group(2);
|
||||||
|
|
||||||
|
if (visited.contains(key)) {
|
||||||
|
log.warn("Circular placeholder reference detected: {}", key);
|
||||||
|
result.append(matcher.group(0)); // Keep as is
|
||||||
|
} else {
|
||||||
|
String value = properties.get(key);
|
||||||
|
if (value == null) {
|
||||||
|
value = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
visited.add(key);
|
||||||
|
try {
|
||||||
|
result.append(resolveInternal(value, properties, visited));
|
||||||
|
} finally {
|
||||||
|
visited.remove(key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("Property not found and no default value for: {}", key);
|
||||||
|
result.append(matcher.group(0)); // Keep as is if not resolvable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastMatchEnd = matcher.end();
|
||||||
|
}
|
||||||
|
result.append(text.substring(lastMatchEnd));
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
package click.kamil.springstatemachineexporter.ast.app;
|
package click.kamil.springstatemachineexporter.ast.app;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.model.Action;
|
import click.kamil.springstatemachineexporter.model.Action;
|
||||||
import click.kamil.springstatemachineexporter.model.Guard;
|
import click.kamil.springstatemachineexporter.model.Guard;
|
||||||
import click.kamil.springstatemachineexporter.model.State;
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
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.SimpleName;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -24,8 +18,11 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class AstTransitionParser {
|
public class AstTransitionParser {
|
||||||
|
private static final ConstantResolver constantResolver = new ConstantResolver();
|
||||||
|
|
||||||
private static final Set<String> SUPPORTED_TRANSITION_METHOD_NAMES = Arrays.stream(TransitionType.values())
|
private static final Set<String> SUPPORTED_TRANSITION_METHOD_NAMES = Arrays.stream(TransitionType.values())
|
||||||
.map(TransitionType::getMethodName)
|
.map(TransitionType::getMethodName)
|
||||||
@@ -173,15 +170,15 @@ public class AstTransitionParser {
|
|||||||
Expression secondArg = resolveArg((Expression) args.get(1), argsMap);
|
Expression secondArg = resolveArg((Expression) args.get(1), argsMap);
|
||||||
if ("last".equals(methodName)) {
|
if ("last".equals(methodName)) {
|
||||||
// For 'last', the second argument is an action, not a guard
|
// For 'last', the second argument is an action, not a guard
|
||||||
parseAction(secondArg, t);
|
parseAction(secondArg, t, cu, context);
|
||||||
} else {
|
} else {
|
||||||
// For 'first' and 'then', the second argument is a guard
|
// For 'first' and 'then', the second argument is a guard
|
||||||
parseGuard(secondArg, t);
|
parseGuard(secondArg, t, cu, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (args.size() > 2) {
|
if (args.size() > 2) {
|
||||||
Expression thirdArg = resolveArg((Expression) args.get(2), argsMap);
|
Expression thirdArg = resolveArg((Expression) args.get(2), argsMap);
|
||||||
parseAction(thirdArg, t);
|
parseAction(thirdArg, t, cu, context);
|
||||||
}
|
}
|
||||||
t.setOrder(orderCounter++);
|
t.setOrder(orderCounter++);
|
||||||
transitions.add(t);
|
transitions.add(t);
|
||||||
@@ -215,32 +212,328 @@ public class AstTransitionParser {
|
|||||||
});
|
});
|
||||||
case "event" -> {
|
case "event" -> {
|
||||||
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
t.setEvent(QuotedExpression.of(resolved).toStringWithoutQuotes());
|
String eventValue = constantResolver.resolve(resolved, context);
|
||||||
|
if (eventValue != null) {
|
||||||
|
t.setEvent(Event.of(resolved.toString(), eventValue));
|
||||||
|
} else if (resolved instanceof QualifiedName qn) {
|
||||||
|
String qualifier = qn.getQualifier().toString();
|
||||||
|
String name = qn.getName().getIdentifier();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(qualifier, cu);
|
||||||
|
if (td != null) {
|
||||||
|
t.setEvent(Event.of(resolved.toString(), context.getFqn(td) + "." + name));
|
||||||
|
} else {
|
||||||
|
t.setEvent(Event.of(resolved.toString(), qn.getFullyQualifiedName()));
|
||||||
|
}
|
||||||
|
} else if (resolved instanceof SimpleName sn) {
|
||||||
|
String name = sn.getIdentifier();
|
||||||
|
String full = name;
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + name)) {
|
||||||
|
full = impName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.setEvent(Event.of(resolved.toString(), full));
|
||||||
|
} else {
|
||||||
|
t.setEvent(Event.of(QuotedExpression.of(resolved).toStringWithoutQuotes()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "guard" -> {
|
case "guard", "guardExpression" -> {
|
||||||
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
parseGuard(resolved, t);
|
parseGuard(resolved, t, cu, context);
|
||||||
}
|
}
|
||||||
case "action" -> {
|
case "action" -> {
|
||||||
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
parseAction(resolved, t);
|
parseAction(resolved, t, cu, context);
|
||||||
|
}
|
||||||
|
case "timer", "timerOnce" -> {
|
||||||
|
Expression resolved = resolveArg((Expression) args.get(0), argsMap);
|
||||||
|
t.setEvent(Event.of(methodName + "(" + resolved.toString() + ")", methodName + "(" + resolved.toString() + ")"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void parseGuard(Object arg, Transition t) {
|
private static String extractInternalLogic(Expression expr, CompilationUnit cu, CodebaseContext context) {
|
||||||
|
if (expr == null) return null;
|
||||||
|
|
||||||
|
// 1. Lambda Expressions
|
||||||
|
if (expr instanceof LambdaExpression le) {
|
||||||
|
return le.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Anonymous Classes
|
||||||
|
if (expr instanceof ClassInstanceCreation cic && cic.getAnonymousClassDeclaration() != null) {
|
||||||
|
return cic.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Method Invocations
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
IMethodBinding binding = mi.resolveMethodBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
// Try to resolve deeper logic from the return type (e.g., a Bean returning an Action interface)
|
||||||
|
ITypeBinding returnType = binding.getReturnType();
|
||||||
|
if (returnType != null) {
|
||||||
|
String returnFqn = returnType.getTypeDeclaration().getQualifiedName();
|
||||||
|
TypeDeclaration returnTd = context.getTypeDeclaration(returnFqn);
|
||||||
|
if (returnTd != null) {
|
||||||
|
for (MethodDeclaration rmd : returnTd.getMethods()) {
|
||||||
|
String name = rmd.getName().getIdentifier();
|
||||||
|
if (name.equals("execute") || name.equals("evaluate")) {
|
||||||
|
return rmd.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try same file
|
||||||
|
ASTNode declNode = cu.findDeclaringNode(binding.getKey());
|
||||||
|
if (declNode instanceof MethodDeclaration md) {
|
||||||
|
if (md.getBody() != null && md.getBody().statements().size() == 1) {
|
||||||
|
Object stmt = md.getBody().statements().get(0);
|
||||||
|
if (stmt instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||||
|
Expression retExpr = rs.getExpression();
|
||||||
|
if (isLambdaOrAnonymous(retExpr)) {
|
||||||
|
return retExpr.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return md.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try other files via CodebaseContext
|
||||||
|
ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
String fqn = declaringClass.getTypeDeclaration().getQualifiedName();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(fqn);
|
||||||
|
if (td != null) {
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.getName().getIdentifier().equals(binding.getName()) &&
|
||||||
|
md.parameters().size() == binding.getParameterTypes().length) {
|
||||||
|
|
||||||
|
// Special case: if it's a getter/factory for Action/Guard, look into the return statement
|
||||||
|
if (md.getBody() != null) {
|
||||||
|
if (md.getBody().statements().size() == 1) {
|
||||||
|
Object stmt = md.getBody().statements().get(0);
|
||||||
|
if (stmt instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||||
|
Expression retExpr = rs.getExpression();
|
||||||
|
if (isLambdaOrAnonymous(retExpr)) {
|
||||||
|
return retExpr.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return md.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// FALLBACK: Manual resolution by name if binding failed
|
||||||
|
TypeDeclaration currentClass = findEnclosingType(mi);
|
||||||
|
if (currentClass != null) {
|
||||||
|
return resolveMethodManually(mi.getName().getIdentifier(), currentClass, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Method References
|
||||||
|
if (expr instanceof MethodReference mr) {
|
||||||
|
IMethodBinding binding = mr.resolveMethodBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
String fqn = declaringClass.getTypeDeclaration().getQualifiedName();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(fqn);
|
||||||
|
if (td != null) {
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.getName().getIdentifier().equals(binding.getName()) &&
|
||||||
|
md.parameters().size() == binding.getParameterTypes().length) {
|
||||||
|
return md.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Bean Reference (SimpleName, FieldAccess, etc.)
|
||||||
|
String resolvedBeanBody = resolveBeanClassBody(expr, context);
|
||||||
|
if (resolvedBeanBody != null) {
|
||||||
|
return resolvedBeanBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveBeanClassBody(Expression expr, CodebaseContext context) {
|
||||||
|
String typeFqn = null;
|
||||||
|
ITypeBinding binding = expr.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
typeFqn = binding.getQualifiedName();
|
||||||
|
} else if (expr instanceof SimpleName sn) {
|
||||||
|
// Check enclosing method first for local variables
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
final String[] foundType = {null};
|
||||||
|
final Expression[] foundInitializer = {null};
|
||||||
|
enclosingMethod.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragObj : vds.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(sn.getIdentifier())) {
|
||||||
|
foundType[0] = vds.getType().toString();
|
||||||
|
foundInitializer[0] = fragment.getInitializer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(vds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundInitializer[0] != null) {
|
||||||
|
ASTNode root = expr.getRoot();
|
||||||
|
if (root instanceof CompilationUnit cu) {
|
||||||
|
String initLogic = extractInternalLogic(foundInitializer[0], cu, context);
|
||||||
|
if (initLogic != null) return initLogic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundType[0] != null) {
|
||||||
|
typeFqn = foundType[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFqn == null) {
|
||||||
|
TypeDeclaration enclosingClass = findEnclosingType(expr);
|
||||||
|
if (enclosingClass != null) {
|
||||||
|
for (FieldDeclaration fd : enclosingClass.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(sn.getIdentifier())) {
|
||||||
|
if (fragment.getInitializer() != null) {
|
||||||
|
ASTNode root = expr.getRoot();
|
||||||
|
if (root instanceof CompilationUnit cu) {
|
||||||
|
String initLogic = extractInternalLogic(fragment.getInitializer(), cu, context);
|
||||||
|
if (initLogic != null) return initLogic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typeFqn = fd.getType().toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFqn != null) {
|
||||||
|
if (typeFqn.contains("<")) {
|
||||||
|
typeFqn = typeFqn.substring(0, typeFqn.indexOf('<'));
|
||||||
|
}
|
||||||
|
TypeDeclaration beanTd = context.getTypeDeclaration(typeFqn);
|
||||||
|
if (beanTd == null) {
|
||||||
|
ASTNode root = expr.getRoot();
|
||||||
|
if (root instanceof CompilationUnit cu) {
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
if (!imp.isStatic() && !imp.isOnDemand()) {
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + typeFqn)) {
|
||||||
|
beanTd = context.getTypeDeclaration(impName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (beanTd != null) {
|
||||||
|
for (MethodDeclaration md : beanTd.getMethods()) {
|
||||||
|
String name = md.getName().getIdentifier();
|
||||||
|
if ("evaluate".equals(name) || "execute".equals(name)) {
|
||||||
|
return md.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return beanTd.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode current = node;
|
||||||
|
while (current != null) {
|
||||||
|
if (current instanceof MethodDeclaration md) return md;
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveMethodManually(String methodName, TypeDeclaration td, CodebaseContext context) {
|
||||||
|
// Search in this class
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.getName().getIdentifier().equals(methodName)) {
|
||||||
|
// Found it. Apply the return-statement-extraction logic here too
|
||||||
|
if (md.getBody() != null && md.getBody().statements().size() == 1) {
|
||||||
|
Object stmt = md.getBody().statements().get(0);
|
||||||
|
if (stmt instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
|
||||||
|
Expression retExpr = rs.getExpression();
|
||||||
|
if (isLambdaOrAnonymous(retExpr)) {
|
||||||
|
return retExpr.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return md.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in superclass
|
||||||
|
if (td.getSuperclassType() != null) {
|
||||||
|
String superName = td.getSuperclassType().toString();
|
||||||
|
// Try to resolve simple name to FQN if possible, or just search by simple name
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(superName);
|
||||||
|
if (superTd != null) {
|
||||||
|
return resolveMethodManually(methodName, superTd, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
|
ASTNode current = node;
|
||||||
|
while (current != null) {
|
||||||
|
if (current instanceof TypeDeclaration td) return td;
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseGuard(Object arg, Transition t, CompilationUnit cu, CodebaseContext context) {
|
||||||
QuotedExpression quotedExpr = QuotedExpression.of(arg);
|
QuotedExpression quotedExpr = QuotedExpression.of(arg);
|
||||||
if (quotedExpr != null) {
|
if (quotedExpr != null) {
|
||||||
t.setGuard(Guard.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression())));
|
String internalLogic = extractInternalLogic(quotedExpr.getExpression(), cu, context);
|
||||||
|
int lineNumber = cu.getLineNumber(quotedExpr.getExpression().getStartPosition());
|
||||||
|
TypeDeclaration td = findEnclosingType(quotedExpr.getExpression());
|
||||||
|
String fqn = td != null ? context.getFqn(td) : null;
|
||||||
|
String sourceFile = fqn != null ? context.getRelativePath(fqn) : null;
|
||||||
|
t.setGuard(Guard.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression()), internalLogic, lineNumber, fqn, sourceFile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void parseAction(Object arg, Transition t) {
|
private static void parseAction(Object arg, Transition t, CompilationUnit cu, CodebaseContext context) {
|
||||||
QuotedExpression quotedExpr = QuotedExpression.of(arg);
|
QuotedExpression quotedExpr = QuotedExpression.of(arg);
|
||||||
if (quotedExpr != null) {
|
if (quotedExpr != null) {
|
||||||
t.getActions().add(Action.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression())));
|
String internalLogic = extractInternalLogic(quotedExpr.getExpression(), cu, context);
|
||||||
|
int lineNumber = cu.getLineNumber(quotedExpr.getExpression().getStartPosition());
|
||||||
|
TypeDeclaration td = findEnclosingType(quotedExpr.getExpression());
|
||||||
|
String fqn = td != null ? context.getFqn(td) : null;
|
||||||
|
String sourceFile = fqn != null ? context.getRelativePath(fqn) : null;
|
||||||
|
t.getActions().add(Action.of(quotedExpr.toStringWithoutQuotes(), isLambdaOrAnonymous(quotedExpr.getExpression()), internalLogic, lineNumber, fqn, sourceFile));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,620 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.app;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class StateMachineAdapterParser {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
private enum AnalysisGoal {TRANSITIONS, STATES}
|
||||||
|
|
||||||
|
public record AdapterStates(Set<String> initialStates, Set<String> endStates) {}
|
||||||
|
|
||||||
|
public record AdapterConfig(
|
||||||
|
List<Transition> transitions,
|
||||||
|
Set<String> initialStates,
|
||||||
|
Set<String> endStates
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public StateMachineAdapterParser(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdapterConfig parse(TypeDeclaration startClass) {
|
||||||
|
List<Transition> transitions = parseTransitions(startClass);
|
||||||
|
AdapterStates states = parseStates(startClass);
|
||||||
|
return new AdapterConfig(transitions, states.initialStates(), states.endStates());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Transition> parseTransitions(TypeDeclaration startClass) {
|
||||||
|
List<Transition> allTransitions = new ArrayList<>();
|
||||||
|
Set<MethodVisit> visitedMethods = new HashSet<>();
|
||||||
|
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
|
||||||
|
return allTransitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdapterStates parseStates(TypeDeclaration startClass) {
|
||||||
|
Set<String> initialStates = new HashSet<>();
|
||||||
|
Set<String> endStates = new HashSet<>();
|
||||||
|
Set<MethodVisit> visitedMethods = new HashSet<>();
|
||||||
|
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
|
||||||
|
return new AdapterStates(initialStates, endStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
if (currentTd == null)
|
||||||
|
return;
|
||||||
|
String fqn = context.getFqn(currentTd);
|
||||||
|
if (visitedClasses.contains(fqn))
|
||||||
|
return;
|
||||||
|
visitedClasses.add(fqn);
|
||||||
|
|
||||||
|
for (MethodDeclaration method : currentTd.getMethods()) {
|
||||||
|
if (isConfigureTransitionsMethod(method)) {
|
||||||
|
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
||||||
|
|
||||||
|
// Hierarchy - Superclass
|
||||||
|
Type superclassType = currentTd.getSuperclassType();
|
||||||
|
if (superclassType != null) {
|
||||||
|
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
||||||
|
if (superclassTd != null) {
|
||||||
|
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hierarchy - Interfaces
|
||||||
|
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
||||||
|
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||||
|
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
||||||
|
if (interfaceTd != null) {
|
||||||
|
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
||||||
|
if (visitedMethods.contains(visit)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
visitedMethods.add(visit);
|
||||||
|
|
||||||
|
Block body = method.getBody();
|
||||||
|
if (body == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
if (isTransitionChainEntry(mi)) {
|
||||||
|
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
|
||||||
|
} else if (isTransitionChainContinuation(mi, argsMap)) {
|
||||||
|
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
|
||||||
|
} else if (isForEach(mi)) {
|
||||||
|
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||||
|
} else {
|
||||||
|
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
||||||
|
Expression lambdaReceiver = mi.getExpression();
|
||||||
|
if (lambdaReceiver instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof LambdaExpression lambda) {
|
||||||
|
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||||
|
if (calledMethod == null) {
|
||||||
|
// Try to resolve method in another class if it's a static call or on a known type
|
||||||
|
Expression miReceiver = mi.getExpression();
|
||||||
|
if (miReceiver instanceof SimpleName sn) {
|
||||||
|
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
|
||||||
|
if (otherTd != null) {
|
||||||
|
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
|
||||||
|
if (calledMethod != null) {
|
||||||
|
// For calls to other classes, we switch the "searchStartClass" to that class
|
||||||
|
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||||
|
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
|
||||||
|
return transitions; // Handled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calledMethod != null) {
|
||||||
|
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||||
|
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isForEach(MethodInvocation mi) {
|
||||||
|
return mi.getName().getIdentifier().equals("forEach");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||||
|
List<?> params = lambda.parameters();
|
||||||
|
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
|
||||||
|
Object p = params.get(i);
|
||||||
|
String paramName = "";
|
||||||
|
if (p instanceof VariableDeclaration vp)
|
||||||
|
paramName = vp.getName().getIdentifier();
|
||||||
|
if (!paramName.isEmpty()) {
|
||||||
|
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||||
|
if (lambda.getBody() instanceof Block block)
|
||||||
|
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||||
|
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
|
||||||
|
} else {
|
||||||
|
if (lambda.getBody() instanceof Block block)
|
||||||
|
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||||
|
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
Expression receiver = forEachMi.getExpression();
|
||||||
|
List<Expression> elements = unrollCollection(receiver, argsMap);
|
||||||
|
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
|
||||||
|
return transitions;
|
||||||
|
|
||||||
|
Object lambdaObj = forEachMi.arguments().get(0);
|
||||||
|
if (lambdaObj instanceof LambdaExpression lambda) {
|
||||||
|
String paramName = getLambdaParamName(lambda);
|
||||||
|
for (Expression element : elements) {
|
||||||
|
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||||
|
if (!paramName.isEmpty())
|
||||||
|
lambdaArgsMap.put(paramName, element);
|
||||||
|
|
||||||
|
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||||
|
if (lambda.getBody() instanceof Block block)
|
||||||
|
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||||
|
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
|
||||||
|
} else {
|
||||||
|
if (lambda.getBody() instanceof Block block)
|
||||||
|
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||||
|
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
|
||||||
|
String paramName = efs.getParameter().getName().getIdentifier();
|
||||||
|
|
||||||
|
for (Expression element : elements) {
|
||||||
|
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
|
||||||
|
loopArgsMap.put(paramName, element);
|
||||||
|
|
||||||
|
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||||
|
if (efs.getBody() instanceof Block block)
|
||||||
|
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
|
||||||
|
else if (efs.getBody() instanceof ExpressionStatement es)
|
||||||
|
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
|
||||||
|
} else {
|
||||||
|
if (efs.getBody() instanceof Block block)
|
||||||
|
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
||||||
|
else if (efs.getBody() instanceof ExpressionStatement es)
|
||||||
|
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
|
||||||
|
Set<Expression> visited = new HashSet<>();
|
||||||
|
Expression current = expr;
|
||||||
|
while (current != null) {
|
||||||
|
if (!visited.add(current))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (current instanceof MethodInvocation mi) {
|
||||||
|
String name = mi.getName().getIdentifier();
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|
||||||
|
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
|
||||||
|
if (mi.arguments().size() == 1) {
|
||||||
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
|
if (arg instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof List list)
|
||||||
|
return (List<Expression>) list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (List<Expression>) mi.arguments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof List list)
|
||||||
|
return (List<Expression>) list;
|
||||||
|
if (resolved instanceof Expression e) {
|
||||||
|
current = e;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current instanceof ArrayInitializer ai)
|
||||||
|
return (List<Expression>) ai.expressions();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLambdaParamName(LambdaExpression lambda) {
|
||||||
|
if (lambda.parameters().size() == 1) {
|
||||||
|
Object p = lambda.parameters().get(0);
|
||||||
|
if (p instanceof VariableDeclaration vp)
|
||||||
|
return vp.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
List<Transition> transitions = new ArrayList<>();
|
||||||
|
for (Object stmt : block.statements()) {
|
||||||
|
if (stmt instanceof ExpressionStatement es)
|
||||||
|
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
|
||||||
|
else if (stmt instanceof VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragmentObj : vds.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
Expression initializer = fragment.getInitializer();
|
||||||
|
if (initializer != null) {
|
||||||
|
Object resolved = resolve(initializer, argsMap);
|
||||||
|
if (resolved != null)
|
||||||
|
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
||||||
|
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (stmt instanceof EnhancedForStatement efs)
|
||||||
|
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||||
|
else if (stmt instanceof TryStatement ts)
|
||||||
|
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
|
||||||
|
}
|
||||||
|
return transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
for (Object stmt : block.statements()) {
|
||||||
|
if (stmt instanceof ExpressionStatement es)
|
||||||
|
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
else if (stmt instanceof VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragmentObj : vds.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
Expression initializer = fragment.getInitializer();
|
||||||
|
if (initializer != null) {
|
||||||
|
Object resolved = resolve(initializer, argsMap);
|
||||||
|
if (resolved != null)
|
||||||
|
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
||||||
|
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (stmt instanceof EnhancedForStatement efs)
|
||||||
|
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||||
|
else if (stmt instanceof TryStatement ts)
|
||||||
|
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
if (isWithStatesChain(mi, argsMap))
|
||||||
|
parseStateChain(mi, initialStates, endStates, argsMap);
|
||||||
|
else if (isForEach(mi))
|
||||||
|
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||||
|
else {
|
||||||
|
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved instanceof LambdaExpression lambda) {
|
||||||
|
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||||
|
if (calledMethod != null) {
|
||||||
|
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||||
|
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
|
||||||
|
Map<String, Object> map = new HashMap<>(currentArgsMap);
|
||||||
|
List<?> params = method.parameters();
|
||||||
|
List<?> args = call.arguments();
|
||||||
|
|
||||||
|
for (int i = 0; i < params.size(); i++) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
|
||||||
|
String paramName = param.getName().getIdentifier();
|
||||||
|
if (param.isVarargs() && i < params.size()) {
|
||||||
|
List<Expression> varargList = new ArrayList<>();
|
||||||
|
for (int j = i; j < args.size(); j++) {
|
||||||
|
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
|
||||||
|
if (resolved instanceof List<?> list) {
|
||||||
|
for (Object item : list) {
|
||||||
|
if (item instanceof Expression e)
|
||||||
|
varargList.add(e);
|
||||||
|
}
|
||||||
|
} else if (resolved instanceof Expression e) {
|
||||||
|
varargList.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map.put(paramName, varargList);
|
||||||
|
} else if (i < args.size()) {
|
||||||
|
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object resolve(Expression expr, Map<String, Object> argsMap) {
|
||||||
|
if (expr instanceof NullLiteral)
|
||||||
|
return null;
|
||||||
|
Set<Expression> visited = new HashSet<>();
|
||||||
|
Object current = expr;
|
||||||
|
while (current instanceof SimpleName sn) {
|
||||||
|
if (!visited.add((SimpleName) current))
|
||||||
|
break;
|
||||||
|
Object resolved = argsMap.get(sn.getIdentifier());
|
||||||
|
if (resolved == null || resolved == current)
|
||||||
|
break;
|
||||||
|
current = resolved;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
|
||||||
|
Object resolved = resolve(expr, argsMap);
|
||||||
|
if (resolved instanceof Expression e)
|
||||||
|
return e;
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
|
||||||
|
Map<String, Expression> map = new HashMap<>();
|
||||||
|
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
|
||||||
|
if (entry.getValue() instanceof Expression e)
|
||||||
|
map.put(entry.getKey(), e);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTransitionChainEntry(MethodInvocation mi) {
|
||||||
|
Expression current = mi;
|
||||||
|
while (current instanceof MethodInvocation call) {
|
||||||
|
String name = call.getName().getIdentifier();
|
||||||
|
if (TransitionType.fromMethodName(name).isPresent())
|
||||||
|
return true;
|
||||||
|
current = call.getExpression();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
|
||||||
|
Expression current = mi;
|
||||||
|
Set<Expression> visited = new HashSet<>();
|
||||||
|
while (current instanceof MethodInvocation call) {
|
||||||
|
if (!visited.add(current))
|
||||||
|
break;
|
||||||
|
String name = call.getName().getIdentifier();
|
||||||
|
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
|
||||||
|
Expression receiver = call.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object resolved = resolve(receiver, argsMap);
|
||||||
|
if (resolved instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return resolved == null; // null means it's a parameter we track
|
||||||
|
}
|
||||||
|
if (TransitionType.fromMethodName(name).isPresent())
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
|
||||||
|
return findMethodInHierarchy(methodName, td, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
|
||||||
|
if (td == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
if (visited.contains(fqn))
|
||||||
|
return null;
|
||||||
|
visited.add(fqn);
|
||||||
|
|
||||||
|
for (MethodDeclaration method : td.getMethods())
|
||||||
|
if (method.getName().getIdentifier().equals(methodName))
|
||||||
|
return method;
|
||||||
|
|
||||||
|
Type superclassType = td.getSuperclassType();
|
||||||
|
if (superclassType != null) {
|
||||||
|
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
|
||||||
|
return findMethodInHierarchy(methodName, superclassTd, visited);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRelevantMethod(MethodDeclaration method) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
|
||||||
|
for (MethodDeclaration method : td.getMethods())
|
||||||
|
if (isConfigureTransitionsMethod(method))
|
||||||
|
return method;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
|
||||||
|
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
||||||
|
return false;
|
||||||
|
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
if (currentTd == null)
|
||||||
|
return;
|
||||||
|
String fqn = context.getFqn(currentTd);
|
||||||
|
if (visitedClasses.contains(fqn))
|
||||||
|
return;
|
||||||
|
visitedClasses.add(fqn);
|
||||||
|
|
||||||
|
for (MethodDeclaration method : currentTd.getMethods()) {
|
||||||
|
if (isConfigureStatesMethod(method)) {
|
||||||
|
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
||||||
|
|
||||||
|
// Hierarchy - Superclass
|
||||||
|
Type superclassType = currentTd.getSuperclassType();
|
||||||
|
if (superclassType != null) {
|
||||||
|
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
||||||
|
if (superclassTd != null)
|
||||||
|
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hierarchy - Interfaces
|
||||||
|
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
||||||
|
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||||
|
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
||||||
|
if (interfaceTd != null)
|
||||||
|
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||||
|
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
||||||
|
if (visitedMethods.contains(visit))
|
||||||
|
return;
|
||||||
|
visitedMethods.add(visit);
|
||||||
|
Block body = method.getBody();
|
||||||
|
if (body == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
|
||||||
|
Expression current = mi;
|
||||||
|
Set<Expression> visited = new HashSet<>();
|
||||||
|
while (current instanceof MethodInvocation call) {
|
||||||
|
if (!visited.add(current))
|
||||||
|
break;
|
||||||
|
if (call.getName().getIdentifier().equals("withStates"))
|
||||||
|
return true;
|
||||||
|
Expression receiver = call.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object resolved = resolve(receiver, argsMap);
|
||||||
|
if (resolved instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
|
||||||
|
List<MethodInvocation> chain = new ArrayList<>();
|
||||||
|
Expression current = mi;
|
||||||
|
while (current instanceof MethodInvocation m) {
|
||||||
|
chain.addFirst(m);
|
||||||
|
current = m.getExpression();
|
||||||
|
}
|
||||||
|
CompilationUnit cu = (CompilationUnit) mi.getRoot();
|
||||||
|
for (MethodInvocation call : chain) {
|
||||||
|
String methodName = call.getName().getIdentifier();
|
||||||
|
if (call.arguments().isEmpty())
|
||||||
|
continue;
|
||||||
|
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
|
||||||
|
if ("initial".equals(methodName)) {
|
||||||
|
if (!isInsideRegionOrFork(call)) {
|
||||||
|
initialStates.add(context.resolveState(arg, cu).toString());
|
||||||
|
}
|
||||||
|
} else if ("end".equals(methodName)) {
|
||||||
|
endStates.add(context.resolveState(arg, cu).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
||||||
|
Expression expr = mi.getExpression();
|
||||||
|
while (expr instanceof MethodInvocation parent) {
|
||||||
|
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
|
||||||
|
return true;
|
||||||
|
expr = parent.getExpression();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
|
||||||
|
for (MethodDeclaration method : td.getMethods())
|
||||||
|
if (isConfigureStatesMethod(method))
|
||||||
|
return method;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConfigureStatesMethod(MethodDeclaration method) {
|
||||||
|
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
||||||
|
return false;
|
||||||
|
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSimpleName(Type type) {
|
||||||
|
if (type.isSimpleType())
|
||||||
|
return ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
if (type.isParameterizedType())
|
||||||
|
return getSimpleName(((ParameterizedType) type).getType());
|
||||||
|
return type.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,632 +1,37 @@
|
|||||||
package click.kamil.springstatemachineexporter.ast.app;
|
package click.kamil.springstatemachineexporter.ast.app;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.eclipse.jdt.core.dom.ArrayInitializer;
|
|
||||||
import org.eclipse.jdt.core.dom.Block;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.EnhancedForStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.ExpressionStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.LambdaExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.NullLiteral;
|
|
||||||
import org.eclipse.jdt.core.dom.ParameterizedType;
|
|
||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
|
||||||
import org.eclipse.jdt.core.dom.SimpleType;
|
|
||||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TryStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.Type;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class StateMachineAggregator {
|
public class StateMachineAggregator {
|
||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
|
private final StateMachineAdapterParser adapterParser;
|
||||||
private enum AnalysisGoal {TRANSITIONS, STATES}
|
|
||||||
|
|
||||||
public StateMachineAggregator(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
|
|
||||||
List<Transition> allTransitions = new ArrayList<>();
|
|
||||||
Set<MethodVisit> visitedMethods = new HashSet<>();
|
|
||||||
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
|
|
||||||
return allTransitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
if (currentTd == null)
|
|
||||||
return;
|
|
||||||
String fqn = context.getFqn(currentTd);
|
|
||||||
if (visitedClasses.contains(fqn))
|
|
||||||
return;
|
|
||||||
visitedClasses.add(fqn);
|
|
||||||
|
|
||||||
for (MethodDeclaration method : currentTd.getMethods()) {
|
|
||||||
if (isConfigureTransitionsMethod(method)) {
|
|
||||||
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
|
||||||
|
|
||||||
// Hierarchy - Superclass
|
|
||||||
Type superclassType = currentTd.getSuperclassType();
|
|
||||||
if (superclassType != null) {
|
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
|
||||||
if (superclassTd != null) {
|
|
||||||
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hierarchy - Interfaces
|
|
||||||
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
|
||||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
|
||||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
|
||||||
if (interfaceTd != null) {
|
|
||||||
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
|
||||||
if (visitedMethods.contains(visit)) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
visitedMethods.add(visit);
|
|
||||||
|
|
||||||
Block body = method.getBody();
|
|
||||||
if (body == null) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
if (isTransitionChainEntry(mi)) {
|
|
||||||
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
|
|
||||||
} else if (isTransitionChainContinuation(mi, argsMap)) {
|
|
||||||
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
|
|
||||||
} else if (isForEach(mi)) {
|
|
||||||
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
|
||||||
} else {
|
|
||||||
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
|
||||||
Expression lambdaReceiver = mi.getExpression();
|
|
||||||
if (lambdaReceiver instanceof SimpleName sn) {
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved instanceof LambdaExpression lambda) {
|
|
||||||
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
|
||||||
if (calledMethod == null) {
|
|
||||||
// Try to resolve method in another class if it's a static call or on a known type
|
|
||||||
Expression miReceiver = mi.getExpression();
|
|
||||||
if (miReceiver instanceof SimpleName sn) {
|
|
||||||
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
|
|
||||||
if (otherTd != null) {
|
|
||||||
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
|
|
||||||
if (calledMethod != null) {
|
|
||||||
// For calls to other classes, we switch the "searchStartClass" to that class
|
|
||||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
|
||||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
|
|
||||||
return transitions; // Handled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calledMethod != null) {
|
|
||||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
|
||||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isForEach(MethodInvocation mi) {
|
|
||||||
return mi.getName().getIdentifier().equals("forEach");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
|
||||||
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
|
||||||
List<?> params = lambda.parameters();
|
|
||||||
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
|
|
||||||
Object p = params.get(i);
|
|
||||||
String paramName = "";
|
|
||||||
if (p instanceof VariableDeclaration vp)
|
|
||||||
paramName = vp.getName().getIdentifier();
|
|
||||||
if (!paramName.isEmpty()) {
|
|
||||||
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
|
||||||
if (lambda.getBody() instanceof Block block)
|
|
||||||
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
|
|
||||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
|
||||||
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
|
|
||||||
} else {
|
|
||||||
if (lambda.getBody() instanceof Block block)
|
|
||||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
|
||||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
|
||||||
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
Expression receiver = forEachMi.getExpression();
|
|
||||||
List<Expression> elements = unrollCollection(receiver, argsMap);
|
|
||||||
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
|
|
||||||
return transitions;
|
|
||||||
|
|
||||||
Object lambdaObj = forEachMi.arguments().get(0);
|
|
||||||
if (lambdaObj instanceof LambdaExpression lambda) {
|
|
||||||
String paramName = getLambdaParamName(lambda);
|
|
||||||
for (Expression element : elements) {
|
|
||||||
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
|
||||||
if (!paramName.isEmpty())
|
|
||||||
lambdaArgsMap.put(paramName, element);
|
|
||||||
|
|
||||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
|
||||||
if (lambda.getBody() instanceof Block block)
|
|
||||||
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
|
|
||||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
|
||||||
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
|
|
||||||
} else {
|
|
||||||
if (lambda.getBody() instanceof Block block)
|
|
||||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
|
||||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
|
||||||
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
|
|
||||||
String paramName = efs.getParameter().getName().getIdentifier();
|
|
||||||
|
|
||||||
for (Expression element : elements) {
|
|
||||||
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
|
|
||||||
loopArgsMap.put(paramName, element);
|
|
||||||
|
|
||||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
|
||||||
if (efs.getBody() instanceof Block block)
|
|
||||||
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
|
|
||||||
else if (efs.getBody() instanceof ExpressionStatement es)
|
|
||||||
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
|
|
||||||
} else {
|
|
||||||
if (efs.getBody() instanceof Block block)
|
|
||||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
|
||||||
else if (efs.getBody() instanceof ExpressionStatement es)
|
|
||||||
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
|
|
||||||
Set<Expression> visited = new HashSet<>();
|
|
||||||
Expression current = expr;
|
|
||||||
while (current != null) {
|
|
||||||
if (!visited.add(current))
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (current instanceof MethodInvocation mi) {
|
|
||||||
String name = mi.getName().getIdentifier();
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|
|
||||||
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
|
|
||||||
if (mi.arguments().size() == 1) {
|
|
||||||
Expression arg = (Expression) mi.arguments().get(0);
|
|
||||||
if (arg instanceof SimpleName sn) {
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved instanceof List list)
|
|
||||||
return (List<Expression>) list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (List<Expression>) mi.arguments();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current instanceof SimpleName sn) {
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved instanceof List list)
|
|
||||||
return (List<Expression>) list;
|
|
||||||
if (resolved instanceof Expression e) {
|
|
||||||
current = e;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current instanceof ArrayInitializer ai)
|
|
||||||
return (List<Expression>) ai.expressions();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getLambdaParamName(LambdaExpression lambda) {
|
|
||||||
if (lambda.parameters().size() == 1) {
|
|
||||||
Object p = lambda.parameters().get(0);
|
|
||||||
if (p instanceof VariableDeclaration vp)
|
|
||||||
return vp.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
List<Transition> transitions = new ArrayList<>();
|
|
||||||
for (Object stmt : block.statements()) {
|
|
||||||
if (stmt instanceof ExpressionStatement es)
|
|
||||||
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
|
|
||||||
else if (stmt instanceof VariableDeclarationStatement vds) {
|
|
||||||
for (Object fragmentObj : vds.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
|
||||||
Expression initializer = fragment.getInitializer();
|
|
||||||
if (initializer != null) {
|
|
||||||
Object resolved = resolve(initializer, argsMap);
|
|
||||||
if (resolved != null)
|
|
||||||
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
|
||||||
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (stmt instanceof EnhancedForStatement efs)
|
|
||||||
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
|
||||||
else if (stmt instanceof TryStatement ts)
|
|
||||||
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
|
|
||||||
}
|
|
||||||
return transitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
for (Object stmt : block.statements()) {
|
|
||||||
if (stmt instanceof ExpressionStatement es)
|
|
||||||
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
else if (stmt instanceof VariableDeclarationStatement vds) {
|
|
||||||
for (Object fragmentObj : vds.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
|
||||||
Expression initializer = fragment.getInitializer();
|
|
||||||
if (initializer != null) {
|
|
||||||
Object resolved = resolve(initializer, argsMap);
|
|
||||||
if (resolved != null)
|
|
||||||
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
|
||||||
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (stmt instanceof EnhancedForStatement efs)
|
|
||||||
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
|
||||||
else if (stmt instanceof TryStatement ts)
|
|
||||||
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
if (isWithStatesChain(mi, argsMap))
|
|
||||||
parseStateChain(mi, initialStates, endStates, argsMap);
|
|
||||||
else if (isForEach(mi))
|
|
||||||
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
|
||||||
else {
|
|
||||||
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved instanceof LambdaExpression lambda) {
|
|
||||||
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
|
||||||
if (calledMethod != null) {
|
|
||||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
|
||||||
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
|
|
||||||
Map<String, Object> map = new HashMap<>(currentArgsMap);
|
|
||||||
List<?> params = method.parameters();
|
|
||||||
List<?> args = call.arguments();
|
|
||||||
|
|
||||||
for (int i = 0; i < params.size(); i++) {
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
|
|
||||||
String paramName = param.getName().getIdentifier();
|
|
||||||
if (param.isVarargs() && i < params.size()) {
|
|
||||||
List<Expression> varargList = new ArrayList<>();
|
|
||||||
for (int j = i; j < args.size(); j++) {
|
|
||||||
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
|
|
||||||
if (resolved instanceof List<?> list) {
|
|
||||||
for (Object item : list) {
|
|
||||||
if (item instanceof Expression e)
|
|
||||||
varargList.add(e);
|
|
||||||
}
|
|
||||||
} else if (resolved instanceof Expression e) {
|
|
||||||
varargList.add(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
map.put(paramName, varargList);
|
|
||||||
} else if (i < args.size()) {
|
|
||||||
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object resolve(Expression expr, Map<String, Object> argsMap) {
|
|
||||||
if (expr instanceof NullLiteral)
|
|
||||||
return null;
|
|
||||||
Set<Expression> visited = new HashSet<>();
|
|
||||||
Object current = expr;
|
|
||||||
while (current instanceof SimpleName sn) {
|
|
||||||
if (!visited.add((SimpleName) current))
|
|
||||||
break;
|
|
||||||
Object resolved = argsMap.get(sn.getIdentifier());
|
|
||||||
if (resolved == null || resolved == current)
|
|
||||||
break;
|
|
||||||
current = resolved;
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
|
|
||||||
Object resolved = resolve(expr, argsMap);
|
|
||||||
if (resolved instanceof Expression e)
|
|
||||||
return e;
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
|
|
||||||
Map<String, Expression> map = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
|
|
||||||
if (entry.getValue() instanceof Expression e)
|
|
||||||
map.put(entry.getKey(), e);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTransitionChainEntry(MethodInvocation mi) {
|
|
||||||
Expression current = mi;
|
|
||||||
while (current instanceof MethodInvocation call) {
|
|
||||||
String name = call.getName().getIdentifier();
|
|
||||||
if (TransitionType.fromMethodName(name).isPresent())
|
|
||||||
return true;
|
|
||||||
current = call.getExpression();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
|
|
||||||
Expression current = mi;
|
|
||||||
Set<Expression> visited = new HashSet<>();
|
|
||||||
while (current instanceof MethodInvocation call) {
|
|
||||||
if (!visited.add(current))
|
|
||||||
break;
|
|
||||||
String name = call.getName().getIdentifier();
|
|
||||||
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
|
|
||||||
Expression receiver = call.getExpression();
|
|
||||||
if (receiver instanceof MethodInvocation next) {
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Object resolved = resolve(receiver, argsMap);
|
|
||||||
if (resolved instanceof MethodInvocation next) {
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return resolved == null; // null means it's a parameter we track
|
|
||||||
}
|
|
||||||
if (TransitionType.fromMethodName(name).isPresent())
|
|
||||||
return true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
|
|
||||||
return findMethodInHierarchy(methodName, td, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
|
|
||||||
if (td == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.contains(fqn))
|
|
||||||
return null;
|
|
||||||
visited.add(fqn);
|
|
||||||
|
|
||||||
for (MethodDeclaration method : td.getMethods())
|
|
||||||
if (method.getName().getIdentifier().equals(methodName))
|
|
||||||
return method;
|
|
||||||
|
|
||||||
Type superclassType = td.getSuperclassType();
|
|
||||||
if (superclassType != null) {
|
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
|
|
||||||
return findMethodInHierarchy(methodName, superclassTd, visited);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isRelevantMethod(MethodDeclaration method) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
|
|
||||||
for (MethodDeclaration method : td.getMethods())
|
|
||||||
if (isConfigureTransitionsMethod(method))
|
|
||||||
return method;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
|
|
||||||
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
|
||||||
return false;
|
|
||||||
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final Set<String> initialStates = new HashSet<>();
|
private final Set<String> initialStates = new HashSet<>();
|
||||||
@Getter
|
@Getter
|
||||||
private final Set<String> endStates = new HashSet<>();
|
private final Set<String> endStates = new HashSet<>();
|
||||||
|
|
||||||
|
public StateMachineAggregator(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
this.adapterParser = new StateMachineAdapterParser(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
|
||||||
|
return adapterParser.parseTransitions(startClass);
|
||||||
|
}
|
||||||
|
|
||||||
public void aggregateStates(TypeDeclaration startClass) {
|
public void aggregateStates(TypeDeclaration startClass) {
|
||||||
Set<MethodVisit> visitedMethods = new HashSet<>();
|
StateMachineAdapterParser.AdapterStates states = adapterParser.parseStates(startClass);
|
||||||
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
|
this.initialStates.clear();
|
||||||
}
|
this.initialStates.addAll(states.initialStates());
|
||||||
|
this.endStates.clear();
|
||||||
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
this.endStates.addAll(states.endStates());
|
||||||
if (currentTd == null)
|
|
||||||
return;
|
|
||||||
String fqn = context.getFqn(currentTd);
|
|
||||||
if (visitedClasses.contains(fqn))
|
|
||||||
return;
|
|
||||||
visitedClasses.add(fqn);
|
|
||||||
|
|
||||||
for (MethodDeclaration method : currentTd.getMethods()) {
|
|
||||||
if (isConfigureStatesMethod(method)) {
|
|
||||||
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
|
||||||
|
|
||||||
// Hierarchy - Superclass
|
|
||||||
Type superclassType = currentTd.getSuperclassType();
|
|
||||||
if (superclassType != null) {
|
|
||||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
|
||||||
if (superclassTd != null)
|
|
||||||
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hierarchy - Interfaces
|
|
||||||
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
|
||||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
|
||||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
|
||||||
if (interfaceTd != null)
|
|
||||||
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
|
||||||
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
|
||||||
if (visitedMethods.contains(visit))
|
|
||||||
return;
|
|
||||||
visitedMethods.add(visit);
|
|
||||||
Block body = method.getBody();
|
|
||||||
if (body == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
|
|
||||||
Expression current = mi;
|
|
||||||
Set<Expression> visited = new HashSet<>();
|
|
||||||
while (current instanceof MethodInvocation call) {
|
|
||||||
if (!visited.add(current))
|
|
||||||
break;
|
|
||||||
if (call.getName().getIdentifier().equals("withStates"))
|
|
||||||
return true;
|
|
||||||
Expression receiver = call.getExpression();
|
|
||||||
if (receiver instanceof MethodInvocation next) {
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Object resolved = resolve(receiver, argsMap);
|
|
||||||
if (resolved instanceof MethodInvocation next) {
|
|
||||||
current = next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
|
|
||||||
List<MethodInvocation> chain = new ArrayList<>();
|
|
||||||
Expression current = mi;
|
|
||||||
while (current instanceof MethodInvocation m) {
|
|
||||||
chain.addFirst(m);
|
|
||||||
current = m.getExpression();
|
|
||||||
}
|
|
||||||
CompilationUnit cu = (CompilationUnit) mi.getRoot();
|
|
||||||
for (MethodInvocation call : chain) {
|
|
||||||
String methodName = call.getName().getIdentifier();
|
|
||||||
if (call.arguments().isEmpty())
|
|
||||||
continue;
|
|
||||||
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
|
|
||||||
if ("initial".equals(methodName)) {
|
|
||||||
if (!isInsideRegionOrFork(call)) {
|
|
||||||
initialStates.add(context.resolveState(arg, cu).toString());
|
|
||||||
}
|
|
||||||
} else if ("end".equals(methodName)) {
|
|
||||||
endStates.add(context.resolveState(arg, cu).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
|
||||||
Expression expr = mi.getExpression();
|
|
||||||
while (expr instanceof MethodInvocation parent) {
|
|
||||||
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
|
|
||||||
return true;
|
|
||||||
expr = parent.getExpression();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
|
|
||||||
for (MethodDeclaration method : td.getMethods())
|
|
||||||
if (isConfigureStatesMethod(method))
|
|
||||||
return method;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConfigureStatesMethod(MethodDeclaration method) {
|
|
||||||
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
|
||||||
return false;
|
|
||||||
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getSimpleName(Type type) {
|
|
||||||
if (type.isSimpleType())
|
|
||||||
return ((SimpleType) type).getName().getFullyQualifiedName();
|
|
||||||
if (type.isParameterizedType())
|
|
||||||
return getSimpleName(((ParameterizedType) type).getType());
|
|
||||||
return type.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,23 @@ public class TransitionStateUtils {
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Set<State> findAllStates(Collection<Transition> transitions, Set<String> initialStates, Set<String> endStates) {
|
||||||
|
Set<State> allStates = new java.util.HashSet<>();
|
||||||
|
if (transitions != null) {
|
||||||
|
transitions.forEach(t -> {
|
||||||
|
if (t.getSourceStates() != null) allStates.addAll(t.getSourceStates());
|
||||||
|
if (t.getTargetStates() != null) allStates.addAll(t.getTargetStates());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (initialStates != null) {
|
||||||
|
initialStates.forEach(s -> allStates.add(State.of(s)));
|
||||||
|
}
|
||||||
|
if (endStates != null) {
|
||||||
|
endStates.forEach(s -> allStates.add(State.of(s)));
|
||||||
|
}
|
||||||
|
return allStates;
|
||||||
|
}
|
||||||
|
|
||||||
private static Stream<String> normalizeStates(Collection<State> states) {
|
private static Stream<String> normalizeStates(Collection<State> states) {
|
||||||
return states.stream()
|
return states.stream()
|
||||||
.map(State::toString)
|
.map(State::toString)
|
||||||
|
|||||||
@@ -36,4 +36,63 @@ public final class AstUtils {
|
|||||||
return type.toString(); // fallback
|
return type.toString(); // fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isFunctionalCollectionMethod(org.eclipse.jdt.core.dom.MethodInvocation node) {
|
||||||
|
if (node.getExpression() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding typeBinding = node.getExpression().resolveTypeBinding();
|
||||||
|
if (typeBinding != null) {
|
||||||
|
if (isCollectionOrStreamType(typeBinding)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String methodName = node.getName().getIdentifier();
|
||||||
|
if (methodName.equals("forEach") || methodName.equals("map") ||
|
||||||
|
methodName.equals("filter") || methodName.equals("flatMap") ||
|
||||||
|
methodName.equals("anyMatch") || methodName.equals("allMatch") ||
|
||||||
|
methodName.equals("noneMatch") || methodName.equals("reduce") ||
|
||||||
|
methodName.equals("collect") || methodName.equals("removeIf") ||
|
||||||
|
methodName.equals("ifPresent") || methodName.equals("ifPresentOrElse") ||
|
||||||
|
methodName.equals("compute") || methodName.equals("computeIfAbsent") ||
|
||||||
|
methodName.equals("computeIfPresent") || methodName.equals("replaceAll") ||
|
||||||
|
methodName.equals("merge") || methodName.equals("peek") ||
|
||||||
|
methodName.equals("doOnNext") || methodName.equals("doOnSuccess") ||
|
||||||
|
methodName.equals("doOnError") || methodName.equals("concatMap") ||
|
||||||
|
methodName.equals("switchMap")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCollectionOrStreamType(org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
|
||||||
|
if (typeBinding == null) return false;
|
||||||
|
String fqn = typeBinding.getErasure().getQualifiedName();
|
||||||
|
if (fqn.startsWith("java.util.stream.") ||
|
||||||
|
fqn.equals("java.lang.Iterable") ||
|
||||||
|
fqn.equals("java.util.Collection") ||
|
||||||
|
fqn.equals("java.util.List") ||
|
||||||
|
fqn.equals("java.util.Set") ||
|
||||||
|
fqn.equals("java.util.Map") ||
|
||||||
|
fqn.equals("java.util.Iterator") ||
|
||||||
|
fqn.equals("java.util.Optional") ||
|
||||||
|
fqn.equals("reactor.core.publisher.Flux") ||
|
||||||
|
fqn.equals("reactor.core.publisher.Mono")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (org.eclipse.jdt.core.dom.ITypeBinding intf : typeBinding.getInterfaces()) {
|
||||||
|
if (isCollectionOrStreamType(intf)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeBinding.getSuperclass() != null) {
|
||||||
|
return isCollectionOrStreamType(typeBinding.getSuperclass());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class BindingResolver {
|
||||||
|
|
||||||
|
private BindingResolver() {
|
||||||
|
// Prevent instantiation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding for a given AST Type node.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(Type type) {
|
||||||
|
if (type == null) return null;
|
||||||
|
return type.resolveBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding for a given AST Expression node.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(Expression expression) {
|
||||||
|
if (expression == null) return null;
|
||||||
|
return expression.resolveTypeBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the method binding for a MethodInvocation.
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveMethod(MethodInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveMethodBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a ClassInstanceCreation.
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(ClassInstanceCreation creation) {
|
||||||
|
if (creation == null) return null;
|
||||||
|
return creation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a ConstructorInvocation (e.g. this(...)).
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(ConstructorInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a SuperConstructorInvocation (e.g. super(...)).
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(SuperConstructorInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding of a TypeDeclaration.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(TypeDeclaration td) {
|
||||||
|
if (td == null) return null;
|
||||||
|
return td.resolveBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the fully qualified name of a type binding, safely handling nulls and erasure.
|
||||||
|
*/
|
||||||
|
public static String getFullyQualifiedName(ITypeBinding binding) {
|
||||||
|
if (binding == null) return null;
|
||||||
|
return binding.getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn) {
|
||||||
|
return isSubtypeOf(childBinding, targetFqn, new java.util.HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn, Set<String> visited) {
|
||||||
|
if (childBinding == null || targetFqn == null) return false;
|
||||||
|
|
||||||
|
String key = childBinding.getKey();
|
||||||
|
if (key != null && !visited.add(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct match
|
||||||
|
if (targetFqn.equals(childBinding.getErasure().getQualifiedName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check interfaces
|
||||||
|
for (ITypeBinding intf : childBinding.getInterfaces()) {
|
||||||
|
if (isSubtypeOf(intf, targetFqn, visited)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check superclass
|
||||||
|
ITypeBinding superclass = childBinding.getSuperclass();
|
||||||
|
if (superclass != null) {
|
||||||
|
return isSubtypeOf(superclass, targetFqn, visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class CfgBuilder {
|
||||||
|
|
||||||
|
public static ControlFlowGraph build(MethodDeclaration method) {
|
||||||
|
ControlFlowGraph cfg = new ControlFlowGraph(method);
|
||||||
|
if (method.getBody() == null) {
|
||||||
|
cfg.getEntryNode().addSuccessor(cfg.getExitNode());
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CFG recursively
|
||||||
|
List<ControlFlowGraph.CfgNode> exits = buildFlow(method.getBody(), List.of(cfg.getEntryNode()), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : exits) {
|
||||||
|
exit.addSuccessor(cfg.getExitNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ControlFlowGraph.CfgNode> buildFlow(
|
||||||
|
ASTNode node,
|
||||||
|
List<ControlFlowGraph.CfgNode> sources,
|
||||||
|
ControlFlowGraph cfg) {
|
||||||
|
|
||||||
|
if (node == null || sources.isEmpty()) {
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof Block block) {
|
||||||
|
List<ControlFlowGraph.CfgNode> currentSources = sources;
|
||||||
|
for (Object stmtObj : block.statements()) {
|
||||||
|
currentSources = buildFlow((ASTNode) stmtObj, currentSources, cfg);
|
||||||
|
}
|
||||||
|
return currentSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof IfStatement ifStmt) {
|
||||||
|
// Condition node
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(ifStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> thenExits = buildFlow(ifStmt.getThenStatement(), List.of(condNode), cfg);
|
||||||
|
List<ControlFlowGraph.CfgNode> elseExits;
|
||||||
|
if (ifStmt.getElseStatement() != null) {
|
||||||
|
elseExits = buildFlow(ifStmt.getElseStatement(), List.of(condNode), cfg);
|
||||||
|
} else {
|
||||||
|
elseExits = List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> combinedExits = new ArrayList<>();
|
||||||
|
combinedExits.addAll(thenExits);
|
||||||
|
combinedExits.addAll(elseExits);
|
||||||
|
return combinedExits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof WhileStatement whileStmt) {
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(whileStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(whileStmt.getBody(), List.of(condNode), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(condNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(condNode); // Exit loop when cond is false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof DoStatement doStmt) {
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodySources = new ArrayList<>(sources);
|
||||||
|
bodySources.add(condNode);
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(doStmt.getBody(), bodySources, cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
return List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof ForStatement forStmt) {
|
||||||
|
// Initializers
|
||||||
|
List<ControlFlowGraph.CfgNode> currentSources = sources;
|
||||||
|
for (Object initObj : forStmt.initializers()) {
|
||||||
|
ControlFlowGraph.CfgNode initNode = new ControlFlowGraph.CfgNode((ASTNode) initObj, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(initNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : currentSources) {
|
||||||
|
src.addSuccessor(initNode);
|
||||||
|
}
|
||||||
|
currentSources = List.of(initNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition
|
||||||
|
ControlFlowGraph.CfgNode condNode;
|
||||||
|
if (forStmt.getExpression() != null) {
|
||||||
|
condNode = new ControlFlowGraph.CfgNode(forStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
} else {
|
||||||
|
condNode = new ControlFlowGraph.CfgNode(forStmt, ControlFlowGraph.CfgNodeType.STATEMENT); // Dummy node
|
||||||
|
}
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : currentSources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(forStmt.getBody(), List.of(condNode), cfg);
|
||||||
|
|
||||||
|
// Updaters
|
||||||
|
List<ControlFlowGraph.CfgNode> updaterSources = bodyExits;
|
||||||
|
for (Object updateObj : forStmt.updaters()) {
|
||||||
|
ControlFlowGraph.CfgNode updateNode = new ControlFlowGraph.CfgNode((ASTNode) updateObj, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(updateNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : updaterSources) {
|
||||||
|
src.addSuccessor(updateNode);
|
||||||
|
}
|
||||||
|
updaterSources = List.of(updateNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode src : updaterSources) {
|
||||||
|
src.addSuccessor(condNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof EnhancedForStatement effStmt) {
|
||||||
|
ControlFlowGraph.CfgNode loopNode = new ControlFlowGraph.CfgNode(effStmt.getParameter(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(loopNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(loopNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(effStmt.getBody(), List.of(loopNode), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(loopNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(loopNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof ReturnStatement returnStmt) {
|
||||||
|
ControlFlowGraph.CfgNode retNode = new ControlFlowGraph.CfgNode(returnStmt, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(retNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(retNode);
|
||||||
|
}
|
||||||
|
// Return flows to exit, not to subsequent sequential nodes
|
||||||
|
retNode.addSuccessor(cfg.getExitNode());
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: general statement
|
||||||
|
ControlFlowGraph.CfgNode stmtNode = new ControlFlowGraph.CfgNode(node, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(stmtNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(stmtNode);
|
||||||
|
}
|
||||||
|
return List.of(stmtNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,107 +1,397 @@
|
|||||||
package click.kamil.springstatemachineexporter.ast.common;
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver;
|
||||||
import click.kamil.springstatemachineexporter.model.State;
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
import org.eclipse.jdt.core.dom.AST;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import org.eclipse.jdt.core.dom.ASTParser;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.eclipse.jdt.core.dom.Annotation;
|
import org.eclipse.jdt.core.JavaCore;
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.ImportDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.Modifier;
|
|
||||||
import org.eclipse.jdt.core.dom.QualifiedName;
|
|
||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
|
||||||
import org.eclipse.jdt.core.dom.StringLiteral;
|
|
||||||
import org.eclipse.jdt.core.dom.Type;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CodebaseContext {
|
public class CodebaseContext {
|
||||||
private final Map<String, CompilationUnit> classes = new HashMap<>();
|
private final Map<String, CompilationUnit> classes = new HashMap<>();
|
||||||
private final Map<String, Path> classPaths = new HashMap<>();
|
private final Map<String, Path> classPaths = new HashMap<>();
|
||||||
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
||||||
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
private final Set<String> ambiguousSimpleNames = new HashSet<>();
|
||||||
|
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||||
|
private final 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<>();
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private Path projectRoot;
|
||||||
|
|
||||||
|
public void setProjectRoot(Path root) {
|
||||||
|
this.projectRoot = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRelativePath(Path fullPath) {
|
||||||
|
if (projectRoot != null && fullPath.isAbsolute() && fullPath.startsWith(projectRoot)) {
|
||||||
|
return projectRoot.relativize(fullPath).toString();
|
||||||
|
}
|
||||||
|
return fullPath.getFileName().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRelativePath(String fqn) {
|
||||||
|
Path path = classPaths.get(fqn);
|
||||||
|
return path != null ? getRelativePath(path) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadLibraryHints(Path hintsFile) throws IOException {
|
||||||
|
if (Files.exists(hintsFile)) {
|
||||||
|
System.out.println("Loading hints from " + hintsFile.toAbsolutePath());
|
||||||
|
this.libraryHints = objectMapper.readValue(hintsFile.toFile(), new TypeReference<List<LibraryHint>>() {});
|
||||||
|
System.out.println("Loaded " + libraryHints.size() + " library hints");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LibraryHint> getLibraryHints() {
|
||||||
|
return Collections.unmodifiableList(libraryHints);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public Map<String, Map<String, String>> getProperties() {
|
||||||
|
return Collections.unmodifiableMap(allProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClasspath(List<String> classpath) {
|
||||||
|
this.classpath = classpath.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourcepath(List<String> sourcepath) {
|
||||||
|
this.sourcepath = sourcepath.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResolveBindings(boolean resolveBindings) {
|
||||||
|
this.resolveBindings = resolveBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveProfiles(List<String> profiles) {
|
||||||
|
this.activeProfiles.clear();
|
||||||
|
if (profiles != null) {
|
||||||
|
this.activeProfiles.addAll(profiles);
|
||||||
|
}
|
||||||
|
this.propertyResolver.setActiveProfiles(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveString(String input) {
|
||||||
|
if (input == null) return null;
|
||||||
|
|
||||||
|
Map<String, String> merged = new HashMap<>();
|
||||||
|
// 1. Default properties
|
||||||
|
Map<String, String> defaultProps = allProperties.get("default");
|
||||||
|
if (defaultProps != null) {
|
||||||
|
merged.putAll(defaultProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Active profiles (later ones override earlier ones)
|
||||||
|
for (String profile : activeProfiles) {
|
||||||
|
Map<String, String> profileProps = allProperties.get(profile);
|
||||||
|
if (profileProps != null) {
|
||||||
|
merged.putAll(profileProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlaceholderResolver.resolve(input, merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveExpression(Expression expr) {
|
||||||
|
if (expr == null) return null;
|
||||||
|
|
||||||
|
String astResolved = constantResolver.resolve(expr, this);
|
||||||
|
if (astResolved == null) {
|
||||||
|
astResolved = expr.toString().replaceAll("^\"|\"$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveString(astResolved);
|
||||||
|
}
|
||||||
|
public TypeDeclaration resolveStaticImport(String memberName, CompilationUnit contextCu) {
|
||||||
|
if (contextCu == null) return null;
|
||||||
|
|
||||||
|
for (Object impObj : contextCu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
if (imp.isStatic()) {
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (imp.isOnDemand()) {
|
||||||
|
// Star import: import static com.example.C.*;
|
||||||
|
TypeDeclaration td = getTypeDeclaration(impName, contextCu);
|
||||||
|
if (td != null && (hasField(td, memberName) || hasMethod(td, memberName))) {
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single import: import static com.example.C.A;
|
||||||
|
if (impName.endsWith("." + memberName)) {
|
||||||
|
String typeFqn = impName.substring(0, impName.lastIndexOf('.'));
|
||||||
|
return getTypeDeclaration(typeFqn, contextCu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||||
|
if (fragment.getName().getIdentifier().equals(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public void scan(Path rootDir) throws IOException {
|
public void scan(Path rootDir) throws IOException {
|
||||||
List<Path> javaFiles = FileUtils.findFilesWithExtension(Set.of(rootDir), ".java");
|
scan(Set.of(rootDir), Collections.emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<CompilationUnit> allCus = new ArrayList<>();
|
||||||
|
|
||||||
|
private final Map<String, List<String>> interfaceToImpls = new HashMap<>();
|
||||||
|
private final Map<String, List<String>> enumValues = new HashMap<>();
|
||||||
|
|
||||||
|
public void scan(Set<Path> rootDirs, Set<String> customIgnorePatterns) throws IOException {
|
||||||
|
this.allProperties = propertyResolver.resolveAllProperties(rootDirs);
|
||||||
|
|
||||||
|
Set<String> activeIgnore = new HashSet<>(ignorePatterns);
|
||||||
|
activeIgnore.addAll(customIgnorePatterns);
|
||||||
|
|
||||||
|
List<Path> javaFiles = FileUtils.findFilesWithExtension(rootDirs, ".java", activeIgnore);
|
||||||
|
log.info("CodebaseContext found {} java files across {} root directories", javaFiles.size(), rootDirs.size());
|
||||||
for (Path javaFile : javaFiles) {
|
for (Path javaFile : javaFiles) {
|
||||||
String source = Files.readString(javaFile);
|
String source = Files.readString(javaFile);
|
||||||
CompilationUnit cu = parse(source);
|
CompilationUnit cu = parse(source, javaFile.toAbsolutePath().toString());
|
||||||
|
allCus.add(cu);
|
||||||
|
|
||||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
for (Object type : cu.types()) {
|
for (Object type : cu.types()) {
|
||||||
if (type instanceof TypeDeclaration td) {
|
if (type instanceof TypeDeclaration td) {
|
||||||
String simpleName = td.getName().getIdentifier();
|
indexType(td, packageName, javaFile);
|
||||||
String fqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
|
} else if (type instanceof EnumDeclaration ed) {
|
||||||
classes.put(fqn, cu);
|
indexEnum(ed, packageName, javaFile);
|
||||||
classPaths.put(fqn, javaFile);
|
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration rd) {
|
||||||
|
indexRecord(rd, packageName, javaFile);
|
||||||
if (simpleNameToFqn.containsKey(simpleName)) {
|
|
||||||
String existingFqn = simpleNameToFqn.get(simpleName);
|
|
||||||
if (!existingFqn.equals(fqn)) {
|
|
||||||
ambiguousSimpleNames.add(simpleName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
simpleNameToFqn.put(simpleName, fqn);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {
|
||||||
|
String simpleName = td.getName().getIdentifier();
|
||||||
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|
||||||
|
classes.put(fqn, (CompilationUnit) td.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inheritance Mapping
|
||||||
|
// Track implementations (for both interfaces and superclasses)
|
||||||
|
if (td.superInterfaceTypes() != null) {
|
||||||
|
for (Object itfObj : td.superInterfaceTypes()) {
|
||||||
|
Type itf = (Type) itfObj;
|
||||||
|
String itfName = extractTypeName(itf);
|
||||||
|
interfaceToImpls.computeIfAbsent(itfName, k -> new ArrayList<>()).add(fqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type superclass = td.getSuperclassType();
|
||||||
|
if (superclass != null) {
|
||||||
|
String superName = extractTypeName(superclass);
|
||||||
|
interfaceToImpls.computeIfAbsent(superName, k -> new ArrayList<>()).add(fqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively index nested types
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getImplementations(String interfaceName) {
|
||||||
|
Set<String> allImpls = new HashSet<>();
|
||||||
|
collectImplementations(interfaceName, allImpls, new HashSet<>());
|
||||||
|
System.out.println("GET IMPLEMENTATIONS FOR: " + interfaceName + " -> " + allImpls);
|
||||||
|
return new ArrayList<>(allImpls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
|
||||||
|
if (!visited.add(typeName)) return;
|
||||||
|
|
||||||
|
// Try direct match
|
||||||
|
List<String> directImpls = interfaceToImpls.get(typeName);
|
||||||
|
|
||||||
|
// Try FQN match if input was simple name
|
||||||
|
if (directImpls == null) {
|
||||||
|
String fqn = simpleNameToFqn.get(typeName);
|
||||||
|
if (fqn != null) {
|
||||||
|
directImpls = interfaceToImpls.get(fqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try simple name match if input was FQN
|
||||||
|
if (directImpls == null && typeName.contains(".")) {
|
||||||
|
String simpleName = typeName.substring(typeName.lastIndexOf('.') + 1);
|
||||||
|
directImpls = interfaceToImpls.get(simpleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directImpls != null) {
|
||||||
|
for (String impl : directImpls) {
|
||||||
|
results.add(impl);
|
||||||
|
// Recursively find implementations of the implementation (e.g., subclasses of an abstract class)
|
||||||
|
collectImplementations(impl, results, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) {
|
||||||
|
String simpleName = ed.getName().getIdentifier();
|
||||||
|
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
|
||||||
|
|
||||||
|
classes.put(fqn, (CompilationUnit) ed.getRoot());
|
||||||
|
classPaths.put(fqn, javaFile);
|
||||||
|
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
for (Object o : ed.enumConstants()) {
|
||||||
|
if (o instanceof EnumConstantDeclaration ecd) {
|
||||||
|
values.add(fqn + "." + ecd.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enumValues.put(fqn, values);
|
||||||
|
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;
|
||||||
|
|
||||||
|
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(cleanName);
|
||||||
|
if (fqn != null) return enumValues.get(fqn);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public State resolveState(Expression expr, CompilationUnit cu) {
|
public State resolveState(Expression expr, CompilationUnit cu) {
|
||||||
if (expr == null)
|
return stateResolver.resolveState(expr, cu);
|
||||||
return null;
|
|
||||||
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 resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
|
private CompilationUnit parse(String source, String unitName) {
|
||||||
String qualifier = qn.getQualifier().toString();
|
|
||||||
String name = qn.getName().getIdentifier();
|
|
||||||
TypeDeclaration td = getTypeDeclaration(qualifier, cu);
|
|
||||||
if (td != null)
|
|
||||||
return getFqn(td) + "." + name;
|
|
||||||
return qn.getFullyQualifiedName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveSimpleName(SimpleName sn, CompilationUnit cu) {
|
|
||||||
String name = sn.getIdentifier();
|
|
||||||
if (cu == null)
|
|
||||||
return name;
|
|
||||||
for (Object impObj : cu.imports()) {
|
|
||||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
|
||||||
String impName = imp.getName().getFullyQualifiedName();
|
|
||||||
if (impName.endsWith("." + name))
|
|
||||||
return impName;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CompilationUnit parse(String source) {
|
|
||||||
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||||
parser.setSource(source.toCharArray());
|
parser.setSource(source.toCharArray());
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
parser.setResolveBindings(false);
|
parser.setResolveBindings(resolveBindings);
|
||||||
|
parser.setBindingsRecovery(true);
|
||||||
|
|
||||||
|
Map<String, String> options = JavaCore.getOptions();
|
||||||
|
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
|
||||||
|
parser.setCompilerOptions(options);
|
||||||
|
|
||||||
|
if (resolveBindings) {
|
||||||
|
parser.setUnitName(unitName);
|
||||||
|
parser.setEnvironment(classpath, sourcepath, null, true);
|
||||||
|
}
|
||||||
return (CompilationUnit) parser.createAST(null);
|
return (CompilationUnit) parser.createAST(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +428,14 @@ public class CodebaseContext {
|
|||||||
|
|
||||||
// 2. Check imports in contextCu
|
// 2. Check imports in contextCu
|
||||||
if (contextCu != null) {
|
if (contextCu != null) {
|
||||||
|
for (Object typeObj : contextCu.types()) {
|
||||||
|
if (typeObj instanceof TypeDeclaration tdDecl) {
|
||||||
|
if (tdDecl.getName().getIdentifier().equals(name)) {
|
||||||
|
return tdDecl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (Object impObj : contextCu.imports()) {
|
for (Object impObj : contextCu.imports()) {
|
||||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
String impName = imp.getName().getFullyQualifiedName();
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
@@ -173,26 +471,241 @@ public class CodebaseContext {
|
|||||||
return getTypeDeclaration(name);
|
return getTypeDeclaration(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFqn(TypeDeclaration td) {
|
public String getSuperclassFqn(TypeDeclaration td) {
|
||||||
CompilationUnit cu = (CompilationUnit) td.getRoot();
|
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 = (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null;
|
||||||
|
TypeDeclaration superTd = getTypeDeclaration(superName, cu);
|
||||||
|
return superTd != null ? getFqn(superTd) : superName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractTypeName(Type type) {
|
||||||
|
if (type.isParameterizedType()) {
|
||||||
|
return extractTypeName(((ParameterizedType) type).getType());
|
||||||
|
} else if (type.isSimpleType()) {
|
||||||
|
return ((SimpleType) type).getName().getFullyQualifiedName();
|
||||||
|
} else if (type.isQualifiedType()) {
|
||||||
|
return ((QualifiedType) type).getName().getFullyQualifiedName();
|
||||||
|
}
|
||||||
|
return type.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeSuper) {
|
||||||
|
String superFqn = getSuperclassFqn(td);
|
||||||
|
if (superFqn != null) {
|
||||||
|
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
||||||
|
if (superTd != null) {
|
||||||
|
return findMethodDeclarationLegacy(superTd, methodName, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check interfaces
|
||||||
|
for (Object interfaceTypeObj : td.superInterfaceTypes()) {
|
||||||
|
Type interfaceType = (Type) interfaceTypeObj;
|
||||||
|
String interfaceName = extractTypeName(interfaceType);
|
||||||
|
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null);
|
||||||
|
if (interfaceTd != null) {
|
||||||
|
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = (atd.getRoot() instanceof CompilationUnit) ? (CompilationUnit) atd.getRoot() : null;
|
||||||
|
if (cu == null) {
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||||
String simpleName = td.getName().getIdentifier();
|
String fqn = sb.toString();
|
||||||
return packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
|
return packageName.isEmpty() ? fqn : packageName + "." + fqn;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TypeDeclaration findTypeInCu(CompilationUnit cu, String fqn) {
|
private TypeDeclaration findTypeInCu(CompilationUnit cu, String fqn) {
|
||||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
|
||||||
for (Object type : cu.types()) {
|
for (Object type : cu.types()) {
|
||||||
if (type instanceof TypeDeclaration td) {
|
if (type instanceof TypeDeclaration td) {
|
||||||
String simpleName = td.getName().getIdentifier();
|
TypeDeclaration found = findNestedType(td, fqn);
|
||||||
String typeFqn = packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
|
if (found != null) return found;
|
||||||
if (typeFqn.equals(fqn))
|
|
||||||
return td;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration findNestedType(TypeDeclaration td, String targetFqn) {
|
||||||
|
if (getFqn(td).equals(targetFqn)) return td;
|
||||||
|
for (TypeDeclaration nested : td.getTypes()) {
|
||||||
|
TypeDeclaration found = findNestedType(nested, targetFqn);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public Path getPath(String className) {
|
public Path getPath(String className) {
|
||||||
return classPaths.get(className);
|
return classPaths.get(className);
|
||||||
}
|
}
|
||||||
@@ -242,10 +755,19 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
||||||
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
|
ITypeBinding binding = td.resolveBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
boolean isSubtype = BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.builders.StateMachineConfigurer")
|
||||||
|
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.StateMachineConfigurerAdapter")
|
||||||
|
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter");
|
||||||
|
if (isSubtype) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extendsStateMachineConfigurerAdapterLegacy(td, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td, Set<String> visited) {
|
private boolean extendsStateMachineConfigurerAdapterLegacy(TypeDeclaration td, Set<String> visited) {
|
||||||
String fqn = getFqn(td);
|
String fqn = getFqn(td);
|
||||||
if (visited.contains(fqn)) {
|
if (visited.contains(fqn)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -262,7 +784,7 @@ public class CodebaseContext {
|
|||||||
if (knownAdapters.contains(superclassName))
|
if (knownAdapters.contains(superclassName))
|
||||||
return true;
|
return true;
|
||||||
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
||||||
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd, visited))
|
if (superTd != null && extendsStateMachineConfigurerAdapterLegacy(superTd, visited))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +795,7 @@ public class CodebaseContext {
|
|||||||
if (knownAdapters.contains(interfaceName))
|
if (knownAdapters.contains(interfaceName))
|
||||||
return true;
|
return true;
|
||||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
||||||
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd, visited))
|
if (interfaceTd != null && extendsStateMachineConfigurerAdapterLegacy(interfaceTd, visited))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,4 +843,31 @@ public class CodebaseContext {
|
|||||||
private String getSimpleName(Type type) {
|
private String getSimpleName(Type type) {
|
||||||
return AstUtils.extractSimpleTypeName(type);
|
return AstUtils.extractSimpleTypeName(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Collection<CompilationUnit> getCompilationUnits() {
|
||||||
|
return Collections.unmodifiableCollection(allCus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<TypeDeclaration> getTypeDeclarations() {
|
||||||
|
List<TypeDeclaration> types = new ArrayList<>();
|
||||||
|
for (CompilationUnit cu : allCus) {
|
||||||
|
for (Object typeObj : cu.types()) {
|
||||||
|
if (typeObj instanceof TypeDeclaration td) {
|
||||||
|
types.add(td);
|
||||||
|
// Handle nested types
|
||||||
|
types.addAll(getNestedTypes(td));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TypeDeclaration> getNestedTypes(TypeDeclaration td) {
|
||||||
|
List<TypeDeclaration> nested = new ArrayList<>();
|
||||||
|
for (TypeDeclaration inner : td.getTypes()) {
|
||||||
|
nested.add(inner);
|
||||||
|
nested.addAll(getNestedTypes(inner));
|
||||||
|
}
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package click.kamil.springstatemachineexporter.ast.common;
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.PathMatcher;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -10,6 +13,14 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
public class FileUtils {
|
public class FileUtils {
|
||||||
public static List<Path> findFilesWithExtension(Set<Path> startDirs, String extension) throws RuntimeException {
|
public static List<Path> findFilesWithExtension(Set<Path> startDirs, String extension) throws RuntimeException {
|
||||||
|
return findFilesWithExtension(startDirs, extension, Collections.emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Path> findFilesWithExtension(Set<Path> startDirs, String extension, Set<String> ignorePatterns) throws RuntimeException {
|
||||||
|
List<PathMatcher> matchers = ignorePatterns.stream()
|
||||||
|
.map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
try (Stream<Path> paths = startDirs.stream()
|
try (Stream<Path> paths = startDirs.stream()
|
||||||
.flatMap(startDir -> {
|
.flatMap(startDir -> {
|
||||||
try {
|
try {
|
||||||
@@ -20,6 +31,10 @@ public class FileUtils {
|
|||||||
})) {
|
})) {
|
||||||
return paths
|
return paths
|
||||||
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
|
.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(extension))
|
||||||
|
.filter(p -> matchers.stream().noneMatch(m -> m.matches(p)))
|
||||||
|
.map(Path::toAbsolutePath)
|
||||||
|
.map(Path::normalize)
|
||||||
|
.distinct()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 +45,31 @@ public class ExporterCommand implements Callable<Integer> {
|
|||||||
@Option(names = {"--no-diamonds"}, description = "Render choice/junction states as regular states instead of diamonds.", negatable = true, defaultValue = "true")
|
@Option(names = {"--no-diamonds"}, description = "Render choice/junction states as regular states instead of diamonds.", negatable = true, defaultValue = "true")
|
||||||
private boolean renderChoicesAsDiamonds;
|
private boolean renderChoicesAsDiamonds;
|
||||||
|
|
||||||
|
@Option(names = {"-p", "--profiles"}, description = "Spring profiles to activate for property resolution.", split = ",")
|
||||||
|
private List<String> activeProfiles;
|
||||||
|
|
||||||
|
@Option(names = {"--event"}, description = "Format for events when they are enums: fn (fullName, default), fqn (fully qualified name), sn (short name)", defaultValue = "fn")
|
||||||
|
private click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat;
|
||||||
|
|
||||||
|
@Option(names = {"--state"}, description = "Format for states when they are enums: fn (fullName, default), fqn (fully qualified name), sn (short name)", defaultValue = "fn")
|
||||||
|
private click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat;
|
||||||
|
|
||||||
|
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
|
||||||
|
private boolean debug;
|
||||||
|
|
||||||
|
@Option(names = {"--resolve-classpath"}, description = "Automatically run Maven/Gradle to resolve full classpath dependencies for perfect AST bindings.", defaultValue = "false")
|
||||||
|
private boolean resolveClasspath;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
var out = spec.commandLine().getOut();
|
var out = spec.commandLine().getOut();
|
||||||
var err = spec.commandLine().getErr();
|
var err = spec.commandLine().getErr();
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
|
||||||
|
out.println(CommandLine.Help.Ansi.AUTO.string("@|bold,cyan Diagnostic mode enabled|@"));
|
||||||
|
}
|
||||||
|
|
||||||
if (inputDir == null && jsonFile == null) {
|
if (inputDir == null && jsonFile == null) {
|
||||||
inputDir = Path.of(".");
|
inputDir = Path.of(".");
|
||||||
}
|
}
|
||||||
@@ -66,10 +86,11 @@ public class ExporterCommand implements Callable<Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList();
|
||||||
if (jsonFile != null) {
|
if (jsonFile != null) {
|
||||||
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats);
|
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
|
||||||
} else {
|
} else {
|
||||||
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds);
|
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, resolveClasspath);
|
||||||
}
|
}
|
||||||
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -18,17 +18,21 @@ public class Dot implements StateMachineExporter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String export(String name,
|
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
|
||||||
List<Transition> transitions,
|
|
||||||
Set<String> startStates,
|
|
||||||
Set<String> endStates,
|
|
||||||
ExportOptions options) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("digraph statemachine {\n");
|
sb.append("digraph statemachine {\n");
|
||||||
sb.append(" rankdir=LR;\n");
|
sb.append(" rankdir=LR;\n");
|
||||||
sb.append(" node [shape=rounded, style=filled, fillcolor=white, fontname=\"Arial\"];\n");
|
sb.append(" node [shape=rounded, style=filled, fillcolor=white, fontname=\"Arial\"];\n");
|
||||||
sb.append(" edge [fontname=\"Arial\", fontsize=10];\n\n");
|
sb.append(" edge [fontname=\"Arial\", fontsize=10];\n\n");
|
||||||
|
|
||||||
|
// --- Core State Machine Rendering ---
|
||||||
|
renderCore(result.getTransitions(), result.getStartStates(), result.getEndStates(), options, sb);
|
||||||
|
|
||||||
|
sb.append("}\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderCore(List<Transition> transitions, Set<String> startStates, Set<String> endStates, ExportOptions options, StringBuilder sb) {
|
||||||
// Choices
|
// Choices
|
||||||
Set<String> statesWithChoice = new LinkedHashSet<>();
|
Set<String> statesWithChoice = new LinkedHashSet<>();
|
||||||
Map<String, String> choiceColorMap = new HashMap<>();
|
Map<String, String> choiceColorMap = new HashMap<>();
|
||||||
@@ -37,7 +41,7 @@ public class Dot implements StateMachineExporter {
|
|||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if (t.getType() == TransitionType.CHOICE && t.getSourceStates() != null) {
|
if (t.getType() == TransitionType.CHOICE && t.getSourceStates() != null) {
|
||||||
for (State source : t.getSourceStates()) {
|
for (State source : t.getSourceStates()) {
|
||||||
String simplified = simplify(source.toString());
|
String simplified = simplify(options.formatState(source));
|
||||||
statesWithChoice.add(simplified);
|
statesWithChoice.add(simplified);
|
||||||
if (!choiceColorMap.containsKey(simplified)) {
|
if (!choiceColorMap.containsKey(simplified)) {
|
||||||
choiceColorMap.put(simplified, CHOICE_COLORS[colorIndex % CHOICE_COLORS.length]);
|
choiceColorMap.put(simplified, CHOICE_COLORS[colorIndex % CHOICE_COLORS.length]);
|
||||||
@@ -82,11 +86,11 @@ public class Dot implements StateMachineExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (State rawSourceState : t.getSourceStates()) {
|
for (State rawSourceState : t.getSourceStates()) {
|
||||||
String rawSource = rawSourceState.toString();
|
String rawSource = options.formatState(rawSourceState);
|
||||||
String source = simplify(rawSource);
|
String source = simplify(rawSource);
|
||||||
|
|
||||||
for (State rawTargetState : targets) {
|
for (State rawTargetState : targets) {
|
||||||
String rawTarget = rawTargetState.toString();
|
String rawTarget = options.formatState(rawTargetState);
|
||||||
String target = simplify(rawTarget);
|
String target = simplify(rawTarget);
|
||||||
String edgeSource = source;
|
String edgeSource = source;
|
||||||
|
|
||||||
@@ -96,8 +100,11 @@ public class Dot implements StateMachineExporter {
|
|||||||
|
|
||||||
// Label: event [guard] / actions
|
// Label: event [guard] / actions
|
||||||
StringBuilder label = new StringBuilder();
|
StringBuilder label = new StringBuilder();
|
||||||
if (t.getEvent() != null && !t.getEvent().isBlank()) {
|
if (t.getEvent() != null) {
|
||||||
label.append(escapeDotString(t.getEvent()));
|
String eventStr = options.formatEvent(t.getEvent());
|
||||||
|
if (eventStr != null && !eventStr.isBlank()) {
|
||||||
|
label.append(escapeDotString(eventStr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard
|
// Guard
|
||||||
@@ -182,6 +189,21 @@ public class Dot implements StateMachineExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String export(String name,
|
||||||
|
List<Transition> transitions,
|
||||||
|
Set<String> startStates,
|
||||||
|
Set<String> endStates,
|
||||||
|
ExportOptions options) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("digraph statemachine {\n");
|
||||||
|
sb.append(" rankdir=LR;\n");
|
||||||
|
sb.append(" node [shape=rounded, style=filled, fillcolor=white, fontname=\"Arial\"];\n");
|
||||||
|
sb.append(" edge [fontname=\"Arial\", fontsize=10];\n\n");
|
||||||
|
|
||||||
|
renderCore(transitions, startStates, endStates, options, sb);
|
||||||
|
|
||||||
sb.append("}\n");
|
sb.append("}\n");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.exporter;
|
||||||
|
|
||||||
|
public enum EnumFormat {
|
||||||
|
fn, fqn, sn
|
||||||
|
}
|
||||||
@@ -10,4 +10,39 @@ public class ExportOptions {
|
|||||||
boolean useLambdaGuards = true;
|
boolean useLambdaGuards = true;
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
boolean renderChoicesAsDiamonds = true;
|
boolean renderChoicesAsDiamonds = true;
|
||||||
|
@Builder.Default
|
||||||
|
boolean embedIdentifiers = false;
|
||||||
|
@Builder.Default
|
||||||
|
boolean includeMetadataPane = true;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
EnumFormat eventFormat = EnumFormat.fn;
|
||||||
|
@Builder.Default
|
||||||
|
EnumFormat stateFormat = EnumFormat.fn;
|
||||||
|
|
||||||
|
public String formatState(click.kamil.springstatemachineexporter.model.State state) {
|
||||||
|
if (state == null) return null;
|
||||||
|
return format(state.rawName(), state.fullIdentifier(), stateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String formatEvent(click.kamil.springstatemachineexporter.model.Event event) {
|
||||||
|
if (event == null) return null;
|
||||||
|
return format(event.rawName(), event.fullIdentifier(), eventFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String format(String raw, String fqn, EnumFormat format) {
|
||||||
|
if (fqn == null) return raw;
|
||||||
|
switch (format) {
|
||||||
|
case fqn: return fqn;
|
||||||
|
case sn: return fqn.substring(fqn.lastIndexOf('.') + 1);
|
||||||
|
case fn:
|
||||||
|
int lastDot = fqn.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) {
|
||||||
|
int prevDot = fqn.lastIndexOf('.', lastDot - 1);
|
||||||
|
return prevDot > 0 ? fqn.substring(prevDot + 1) : fqn;
|
||||||
|
}
|
||||||
|
return fqn;
|
||||||
|
default: return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ public class JsonExporter implements StateMachineExporter {
|
|||||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
|
||||||
|
try {
|
||||||
|
java.util.Map<String, Object> output = new java.util.HashMap<>();
|
||||||
|
output.put("name", result.getName());
|
||||||
|
output.put("transitions", result.getTransitions());
|
||||||
|
output.put("startStates", result.getStartStates());
|
||||||
|
output.put("endStates", result.getEndStates());
|
||||||
|
output.put("renderChoicesAsDiamonds", options.isRenderChoicesAsDiamonds());
|
||||||
|
output.put("metadata", result.getMetadata());
|
||||||
|
|
||||||
|
return objectMapper.writeValueAsString(output);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to export to JSON", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String export(String name, List<Transition> transitions, Set<String> startStates, Set<String> endStates, ExportOptions options) {
|
public String export(String name, List<Transition> transitions, Set<String> startStates, Set<String> endStates, ExportOptions options) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,34 +3,20 @@ package click.kamil.springstatemachineexporter.exporter;
|
|||||||
import click.kamil.springstatemachineexporter.model.State;
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.StringUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class PlantUml implements StateMachineExporter {
|
public class PlantUml implements StateMachineExporter {
|
||||||
private final List<String> choiceColors = List.of("FF6347", "4682B4", "32CD32", "FFD700", "6A5ACD", "FF69B4");
|
|
||||||
|
|
||||||
private final String LAMBDA = "λ";
|
private final String LAMBDA = "λ";
|
||||||
|
|
||||||
private String loadBaseStyle() {
|
@Override
|
||||||
try (InputStream is = getClass().getResourceAsStream("/plantuml/default-style.puml")) {
|
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
|
||||||
if (is == null) return "";
|
return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options);
|
||||||
return new BufferedReader(new InputStreamReader(is))
|
|
||||||
.lines()
|
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -42,53 +28,57 @@ public class PlantUml implements StateMachineExporter {
|
|||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("@startuml\n");
|
sb.append("@startuml\n");
|
||||||
|
sb.append("!pragma layout smetana\n");
|
||||||
|
sb.append("set separator none\n");
|
||||||
|
sb.append("hide empty description\n");
|
||||||
|
sb.append("hide stereotype\n");
|
||||||
|
|
||||||
String style = loadBaseStyle();
|
// --- Premium Modern Styles ---
|
||||||
|
sb.append("skinparam state {\n");
|
||||||
|
sb.append(" BackgroundColor white\n");
|
||||||
|
sb.append(" BorderColor #94a3b8\n");
|
||||||
|
sb.append(" BorderThickness 1\n");
|
||||||
|
sb.append(" FontName Inter\n");
|
||||||
|
sb.append(" FontSize 9\n"); // Sleek but readable
|
||||||
|
sb.append(" FontStyle bold\n");
|
||||||
|
sb.append(" RoundCorner 20\n");
|
||||||
|
sb.append(" Padding 1\n");
|
||||||
|
sb.append("}\n");
|
||||||
|
sb.append("skinparam shadowing false\n");
|
||||||
|
sb.append("skinparam ArrowFontName JetBrains Mono\n");
|
||||||
|
sb.append("skinparam ArrowFontSize 8\n");
|
||||||
|
sb.append("skinparam ArrowColor #cbd5e1\n");
|
||||||
|
sb.append("skinparam ArrowThickness 1\n");
|
||||||
|
sb.append("skinparam dpi 110\n");
|
||||||
|
sb.append("skinparam svgLinkTarget _self\n");
|
||||||
|
|
||||||
StringBuilder choiceColorStyles = new StringBuilder();
|
sb.append("\n");
|
||||||
StringBuilder hideChoiceColors = new StringBuilder();
|
|
||||||
for (int i = 0; i < choiceColors.size(); i++) {
|
|
||||||
choiceColorStyles.append(" .choice_color_").append(i)
|
|
||||||
.append(" { LineColor #").append(choiceColors.get(i)).append(" }\n");
|
|
||||||
hideChoiceColors.append("hide <<choice_color_").append(i).append(">> stereotype\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
style = style.replace("/* CHOICE_COLORS_PLACEHOLDER */", choiceColorStyles.toString());
|
|
||||||
style = style.replace("/* HIDE_CHOICE_COLORS_PLACEHOLDER */", hideChoiceColors.toString());
|
|
||||||
|
|
||||||
sb.append(style).append("\n");
|
|
||||||
|
|
||||||
for (String start : startStates) {
|
for (String start : startStates) {
|
||||||
sb.append("[*] --> ").append(simplify(start)).append("\n");
|
sb.append("[*] --> ").append(simplify(start)).append("\n");
|
||||||
}
|
}
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
|
||||||
Set<String> statesWithChoice = new HashSet<>();
|
Set<String> statesWithChoice = new java.util.LinkedHashSet<>();
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if (t.getType() == TransitionType.CHOICE) {
|
if (t.getType() == TransitionType.CHOICE && t.getSourceStates() != null) {
|
||||||
for (State source : t.getSourceStates()) {
|
for (State source : t.getSourceStates()) {
|
||||||
statesWithChoice.add(simplify(source.toString()));
|
statesWithChoice.add(simplify(options.formatState(source)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> choiceStateClassMap = new HashMap<>();
|
|
||||||
int colorIndex = 0;
|
|
||||||
|
|
||||||
for (String state : statesWithChoice) {
|
for (String state : statesWithChoice) {
|
||||||
String className = "choice_color_" + (colorIndex % choiceColors.size());
|
|
||||||
if (options.isRenderChoicesAsDiamonds()) {
|
if (options.isRenderChoicesAsDiamonds()) {
|
||||||
sb.append("state ").append(state).append(" <<choice>>\n");
|
sb.append("state ").append(state).append(" <<choice>>\n");
|
||||||
}
|
}
|
||||||
choiceStateClassMap.put(state, className);
|
|
||||||
colorIndex++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> junctionStates = transitions.stream()
|
Set<String> junctionStates = transitions.stream()
|
||||||
.filter(t -> t.getType() == TransitionType.JUNCTION)
|
.filter(t -> t.getType() == TransitionType.JUNCTION)
|
||||||
.flatMap(t -> t.getSourceStates().stream())
|
.flatMap(t -> t.getSourceStates().stream())
|
||||||
.map(s -> simplify(s.toString()))
|
.map(s -> simplify(options.formatState(s)))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toCollection(java.util.LinkedHashSet::new));
|
||||||
for (String junction : junctionStates) {
|
for (String junction : junctionStates) {
|
||||||
if (options.isRenderChoicesAsDiamonds()) {
|
if (options.isRenderChoicesAsDiamonds()) {
|
||||||
sb.append("state ").append(junction).append(" <<choice>>\n");
|
sb.append("state ").append(junction).append(" <<choice>>\n");
|
||||||
@@ -97,7 +87,6 @@ public class PlantUml implements StateMachineExporter {
|
|||||||
|
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
|
||||||
Set<String> usedEventStereotypes = new HashSet<>();
|
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if (t.getSourceStates() == null || t.getSourceStates().isEmpty())
|
if (t.getSourceStates() == null || t.getSourceStates().isEmpty())
|
||||||
continue;
|
continue;
|
||||||
@@ -107,33 +96,44 @@ public class PlantUml implements StateMachineExporter {
|
|||||||
List<String> targets;
|
List<String> targets;
|
||||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
if (!allowNoTarget) {
|
if (!allowNoTarget) {
|
||||||
targets = t.getSourceStates().stream().map(s -> s.toString()).toList();
|
targets = t.getSourceStates().stream().map(s -> options.formatState(s)).toList();
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targets = t.getTargetStates().stream().map(s -> s.toString()).toList();
|
targets = t.getTargetStates().stream().map(s -> options.formatState(s)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (State rawSourceState : t.getSourceStates()) {
|
for (State rawSourceState : t.getSourceStates()) {
|
||||||
String source = simplify(rawSourceState.toString());
|
String source = simplify(options.formatState(rawSourceState));
|
||||||
|
|
||||||
for (String rawTarget : targets) {
|
for (String rawTarget : targets) {
|
||||||
String target = simplify(rawTarget);
|
String target = simplify(rawTarget);
|
||||||
String transitionSource = source;
|
|
||||||
|
|
||||||
String styleClass = getStyleClass(t, source, choiceStateClassMap);
|
String styleClass = getStyleClass(t, source, Collections.emptyMap());
|
||||||
String color = getLegacyColor(t, source, choiceStateClassMap);
|
String color = getLegacyColor(t, source, Collections.emptyMap());
|
||||||
sb.append(transitionSource).append(" -[#").append(color).append(",bold]-> ").append(target).append(" <<").append(styleClass).append(">>");
|
sb.append(source).append(" -[#").append(color).append(",bold]-> ").append(target);
|
||||||
|
|
||||||
if (t.getEvent() != null && !t.getEvent().isBlank()) {
|
sb.append(" <<").append(styleClass).append(">>");
|
||||||
String eventClass = "e_" + normalize(t.getEvent());
|
|
||||||
sb.append(" <<").append(eventClass).append(">>");
|
|
||||||
usedEventStereotypes.add(eventClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
String label = buildLabel(t, options.isUseLambdaGuards());
|
|
||||||
if (!label.isEmpty()) {
|
String label = buildLabel(t, options);
|
||||||
|
if (options.isEmbedIdentifiers()) {
|
||||||
|
String eventStr = t.getEvent() != null ? options.formatEvent(t.getEvent()) : null;
|
||||||
|
String linkId = eventStr != null && !eventStr.isBlank() ? "link_" + normalize(source) + "__" + normalize(eventStr) : "link_anon_" + normalize(source) + "_" + normalize(target);
|
||||||
|
|
||||||
|
sb.append(" : [[#").append(linkId).append(" ");
|
||||||
|
if (!label.isEmpty()) {
|
||||||
|
String sanitized = label.replace("[", "[")
|
||||||
|
.replace("]", "]")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">");
|
||||||
|
sb.append(sanitized);
|
||||||
|
} else {
|
||||||
|
sb.append(".");
|
||||||
|
}
|
||||||
|
sb.append("]]");
|
||||||
|
} else if (!label.isEmpty()) {
|
||||||
sb.append(" : ").append(label);
|
sb.append(" : ").append(label);
|
||||||
}
|
}
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
@@ -141,10 +141,6 @@ public class PlantUml implements StateMachineExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String eventClass : usedEventStereotypes) {
|
|
||||||
sb.append("hide <<").append(eventClass).append(">> stereotype\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
for (String end : endStates) {
|
for (String end : endStates) {
|
||||||
sb.append(simplify(end)).append(" --> [*]\n");
|
sb.append(simplify(end)).append(" --> [*]\n");
|
||||||
@@ -161,16 +157,9 @@ public class PlantUml implements StateMachineExporter {
|
|||||||
|
|
||||||
private String getLegacyColor(Transition t, String source, Map<String, String> choiceStateClassMap) {
|
private String getLegacyColor(Transition t, String source, Map<String, String> choiceStateClassMap) {
|
||||||
if (t.getType() == null)
|
if (t.getType() == null)
|
||||||
return "000000";
|
return "94a3b8";
|
||||||
return switch (t.getType()) {
|
return switch (t.getType()) {
|
||||||
case CHOICE -> {
|
case CHOICE -> "FF6347";
|
||||||
String className = choiceStateClassMap.get(source);
|
|
||||||
if (className != null && className.startsWith("choice_color_")) {
|
|
||||||
int index = Integer.parseInt(className.substring("choice_color_".length()));
|
|
||||||
yield choiceColors.get(index % choiceColors.size());
|
|
||||||
}
|
|
||||||
yield "000000";
|
|
||||||
}
|
|
||||||
case EXTERNAL -> "1E90FF";
|
case EXTERNAL -> "1E90FF";
|
||||||
case INTERNAL -> "32CD32";
|
case INTERNAL -> "32CD32";
|
||||||
case LOCAL -> "FFA500";
|
case LOCAL -> "FFA500";
|
||||||
@@ -184,7 +173,7 @@ public class PlantUml implements StateMachineExporter {
|
|||||||
if (t.getType() == null)
|
if (t.getType() == null)
|
||||||
return "default";
|
return "default";
|
||||||
return switch (t.getType()) {
|
return switch (t.getType()) {
|
||||||
case CHOICE -> choiceStateClassMap.getOrDefault(source, "choice_type");
|
case CHOICE -> "choice_type";
|
||||||
case EXTERNAL -> "external";
|
case EXTERNAL -> "external";
|
||||||
case INTERNAL -> "internal";
|
case INTERNAL -> "internal";
|
||||||
case LOCAL -> "local";
|
case LOCAL -> "local";
|
||||||
@@ -199,35 +188,33 @@ public class PlantUml implements StateMachineExporter {
|
|||||||
return ".puml";
|
return ".puml";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getGuardText(Transition t, boolean useLambdaGuards) {
|
@Override
|
||||||
if (t.getGuard() == null || StringUtils.isBlank(t.getGuard().expression()))
|
public String simplify(String state) {
|
||||||
return "";
|
if (state == null) return "";
|
||||||
if (useLambdaGuards && t.getGuard().isLambda())
|
if (state.contains("\"")) return state;
|
||||||
return LAMBDA;
|
return state.replaceAll("[^a-zA-Z0-9_.]", "");
|
||||||
return t.getGuard().expression().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getActionsText(Transition t, boolean useLambdaGuards) {
|
private String buildLabel(Transition t, ExportOptions options) {
|
||||||
if (t.getActions() == null || t.getActions().isEmpty())
|
|
||||||
return "";
|
|
||||||
return t.getActions().stream()
|
|
||||||
.map(action -> (useLambdaGuards && action.isLambda()) ? LAMBDA : action.expression())
|
|
||||||
.collect(Collectors.joining(", "));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildLabel(Transition t, boolean useLambdaGuards) {
|
|
||||||
List<String> parts = new ArrayList<>();
|
List<String> parts = new ArrayList<>();
|
||||||
if (t.getEvent() != null && !t.getEvent().isBlank())
|
if (t.getEvent() != null) {
|
||||||
parts.add(simplify(t.getEvent()));
|
String eventStr = options.formatEvent(t.getEvent());
|
||||||
String guard = getGuardText(t, useLambdaGuards);
|
if (eventStr != null && !eventStr.isBlank()) {
|
||||||
if (!guard.isEmpty())
|
parts.add(eventStr);
|
||||||
parts.add("[" + guard + "]");
|
}
|
||||||
String actions = getActionsText(t, useLambdaGuards);
|
}
|
||||||
if (!actions.isEmpty()) {
|
if (t.getGuard() != null) {
|
||||||
if (!parts.isEmpty())
|
String g = t.getGuard().expression();
|
||||||
parts.add("/ " + actions);
|
if (options.isUseLambdaGuards() && t.getGuard().isLambda()) {
|
||||||
else
|
parts.add("[" + LAMBDA + "]");
|
||||||
parts.add(actions);
|
} else {
|
||||||
|
parts.add("[" + g.replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim() + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (t.getActions() != null && !t.getActions().isEmpty()) {
|
||||||
|
parts.add("/ " + t.getActions().stream()
|
||||||
|
.map(a -> (options.isUseLambdaGuards() && a.isLambda()) ? LAMBDA : a.expression().replaceAll("[\\n\\r]+", " ").replaceAll("\\s{2,}", " ").trim())
|
||||||
|
.collect(Collectors.joining(", ")));
|
||||||
}
|
}
|
||||||
Optional.ofNullable(t.getOrder())
|
Optional.ofNullable(t.getOrder())
|
||||||
.filter(order -> t.getType() == TransitionType.CHOICE || t.getType() == TransitionType.JUNCTION)
|
.filter(order -> t.getType() == TransitionType.CHOICE || t.getType() == TransitionType.JUNCTION)
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ public class Scxml implements StateMachineExporter {
|
|||||||
|
|
||||||
private static final String LAMBDA = "λ";
|
private static final String LAMBDA = "λ";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String export(click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result, ExportOptions options) {
|
||||||
|
return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String export(String name,
|
public String export(String name,
|
||||||
List<Transition> transitions,
|
List<Transition> transitions,
|
||||||
@@ -24,10 +29,10 @@ public class Scxml implements StateMachineExporter {
|
|||||||
sb.append(startStates.isEmpty() ? "" : simplify(startStates.iterator().next()));
|
sb.append(startStates.isEmpty() ? "" : simplify(startStates.iterator().next()));
|
||||||
sb.append("\">\n");
|
sb.append("\">\n");
|
||||||
|
|
||||||
Set<String> allStates = new HashSet<>();
|
Set<String> allStates = new java.util.LinkedHashSet<>();
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
t.getSourceStates().forEach(s -> allStates.add(s.toString()));
|
t.getSourceStates().forEach(s -> allStates.add(options.formatState(s)));
|
||||||
t.getTargetStates().forEach(s -> allStates.add(s.toString()));
|
t.getTargetStates().forEach(s -> allStates.add(options.formatState(s)));
|
||||||
}
|
}
|
||||||
allStates.addAll(startStates);
|
allStates.addAll(startStates);
|
||||||
allStates.addAll(endStates);
|
allStates.addAll(endStates);
|
||||||
@@ -36,7 +41,7 @@ public class Scxml implements StateMachineExporter {
|
|||||||
sb.append(" <state id=\"").append(simplify(state)).append("\">\n");
|
sb.append(" <state id=\"").append(simplify(state)).append("\">\n");
|
||||||
|
|
||||||
for (Transition t : transitions) {
|
for (Transition t : transitions) {
|
||||||
if (t.getSourceStates() == null || t.getSourceStates().stream().noneMatch(s -> s.toString().equals(state)))
|
if (t.getSourceStates() == null || t.getSourceStates().stream().noneMatch(s -> options.formatState(s).equals(state)))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
boolean allowNoTarget = t.getType() == TransitionType.JOIN || t.getType() == TransitionType.FORK;
|
boolean allowNoTarget = t.getType() == TransitionType.JOIN || t.getType() == TransitionType.FORK;
|
||||||
@@ -53,7 +58,7 @@ public class Scxml implements StateMachineExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (State target : targets) {
|
for (State target : targets) {
|
||||||
sb.append(renderTransition(t, target, options.isUseLambdaGuards()));
|
sb.append(renderTransition(t, target, options));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sb.append(" </state>\n");
|
sb.append(" </state>\n");
|
||||||
@@ -68,15 +73,18 @@ public class Scxml implements StateMachineExporter {
|
|||||||
return ".scxml.xml";
|
return ".scxml.xml";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String renderTransition(Transition t, State target, boolean useLambdaGuards) {
|
private String renderTransition(Transition t, State target, ExportOptions options) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append(" <transition target=\"").append(simplify(target.toString())).append("\"");
|
sb.append(" <transition target=\"").append(simplify(options.formatState(target))).append("\"");
|
||||||
|
|
||||||
if (t.getEvent() != null && !t.getEvent().isBlank()) {
|
if (t.getEvent() != null) {
|
||||||
sb.append(" event=\"").append(escapeXml(t.getEvent())).append("\"");
|
String eventStr = options.formatEvent(t.getEvent());
|
||||||
|
if (eventStr != null && !eventStr.isBlank()) {
|
||||||
|
sb.append(" event=\"").append(escapeXml(eventStr)).append("\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String guard = getGuardText(t, useLambdaGuards);
|
String guard = getGuardText(t, options.isUseLambdaGuards());
|
||||||
if (!guard.isBlank()) {
|
if (!guard.isBlank()) {
|
||||||
sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
|
sb.append(" cond=\"").append(escapeXml(guard)).append("\"");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package click.kamil.springstatemachineexporter.exporter;
|
package click.kamil.springstatemachineexporter.exporter;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public interface StateMachineExporter {
|
public interface StateMachineExporter {
|
||||||
|
default String export(AnalysisResult result, ExportOptions options) {
|
||||||
|
return export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options);
|
||||||
|
}
|
||||||
|
|
||||||
String export(String name,
|
String export(String name,
|
||||||
List<Transition> transitions,
|
List<Transition> transitions,
|
||||||
Set<String> startStates,
|
Set<String> startStates,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.model;
|
package click.kamil.springstatemachineexporter.model;
|
||||||
|
|
||||||
public record Action(String expression, boolean isLambda) {
|
public record Action(String expression, boolean isLambda, String internalLogic, Integer lineNumber, String className, String sourceFile) {
|
||||||
public static Action of(String expression, boolean isLambda) {
|
public static Action of(String expression, boolean isLambda, String internalLogic, Integer lineNumber, String className, String sourceFile) {
|
||||||
return expression != null ? new Action(expression, isLambda) : null;
|
return expression != null ? new Action(expression, isLambda, internalLogic, lineNumber, className, sourceFile) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user