Compare commits
1 Commits
ai-branch-
...
29e391c472
| Author | SHA1 | Date | |
|---|---|---|---|
| 29e391c472 |
63
AGENTS.md
63
AGENTS.md
@@ -1,63 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Spring State Machine Explorer is a static analysis tool that extracts and visualizes Spring State Machines from source code. It does **not** run Maven/Gradle on target projects.
|
|
||||||
|
|
||||||
## Build System
|
|
||||||
|
|
||||||
- **Java 21** required (toolchain configured in all build.gradle files)
|
|
||||||
- **Gradle** build system with shadow plugin for fat JARs
|
|
||||||
- Run from root: `./gradlew <task>`
|
|
||||||
|
|
||||||
## Key Commands
|
|
||||||
|
|
||||||
### Run the exporter
|
|
||||||
```bash
|
|
||||||
./gradlew :state_machine_exporter:run --args="-i ./my-project -o ./out -f json"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run the HTML generator
|
|
||||||
```bash
|
|
||||||
./gradlew :state_machine_exporter_html:run --args="-i ./my-project -o ./out_html"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update golden test files
|
|
||||||
```bash
|
|
||||||
./gradlew :state_machine_exporter:runGolden
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run tests
|
|
||||||
```bash
|
|
||||||
./gradlew :state_machine_exporter:test
|
|
||||||
./gradlew :state_machine_exporter_html:test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module Structure
|
|
||||||
|
|
||||||
- **state_machine_exporter**: Core Java AST analyzer (Main class: `click.kamil.springstatemachineexporter.Main`)
|
|
||||||
- **state_machine_exporter_html**: HTML portal generator (Main class: `click.kamil.springstatemachineexporter.html.Main`)
|
|
||||||
- **state_machine_exporter_spring_based**: Spring-based exporter sample
|
|
||||||
- **file_combine**: Utility for concatenating Java files
|
|
||||||
- **state_machines/**: Sample state machine projects for testing
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
- **Source-first analysis**: Reads source + build metadata (pom.xml, settings.gradle) statically
|
|
||||||
- **Accessor inlining**: Enabled by default (O(1) lookup for trivial getters/setters). Disable with `--no-inline-accessors`
|
|
||||||
- **Generated sources**: Scans `target/`/`build/` by default. Control with `--include-generated-sources`
|
|
||||||
- **Debug mode**: Use `--debug` flag for verbose logging and unresolved chain diagnostics
|
|
||||||
- **Spring profiles**: Pass with `-p, --profiles` for property placeholder resolution
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- **Golden tests**: JSON/PNG/PUML/DOT/SCXML outputs are compared against golden files in `state_machine_exporter/src/test/resources/golden/`
|
|
||||||
- **E2E tests**: Tagged with `@Tag("e2e")` in `PlantUmlE2ETest.java`
|
|
||||||
- **Test scenarios**: Defined in `TestScenario.java` record with name, inputPath, goldenPath, baseName, activeProfiles
|
|
||||||
|
|
||||||
## Important Constraints
|
|
||||||
|
|
||||||
- **No Maven/Gradle execution**: The tool does not run the target project's build
|
|
||||||
- **JDT bindings**: Limited to library types (Spring Boot 3.2.0, Spring State Machine 3.2.0) - external JARs not resolved
|
|
||||||
- **Monorepo structure**: All modules in root; state_machines/ contains sample projects
|
|
||||||
- **Java 21 only**: All modules require Java 21 toolchain
|
|
||||||
53
README.md
53
README.md
@@ -8,46 +8,16 @@ Static analysis tool to extract and visualize Spring State Machines with deep co
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Analysis is **source-first**: the exporter reads project source and build metadata (`pom.xml`, `settings.gradle`) statically. It does **not** run Maven or Gradle against the project being analyzed.
|
|
||||||
|
|
||||||
### 1. Extract Metadata (JSON)
|
### 1. Extract Metadata (JSON)
|
||||||
```bash
|
```bash
|
||||||
./gradlew :state_machine_exporter:run --args="-i ./my-project -o ./out -f json"
|
./gradlew :state_machine_exporter:run --args="-i ./my-project -o ./out -f json"
|
||||||
```
|
```
|
||||||
|
|
||||||
Common options:
|
|
||||||
|
|
||||||
| Flag | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `-i, --input` | Project directory to scan (defaults to `.`) |
|
|
||||||
| `-j, --json` | Re-render from an existing JSON export instead of scanning source |
|
|
||||||
| `-o, --output` | Output directory (default `./out`) |
|
|
||||||
| `-f, --format` | Comma-separated formats: `json`, `dot`, `puml`, `scxml` |
|
|
||||||
| `-p, --profiles` | Spring profiles for property placeholder resolution |
|
|
||||||
| `--event`, `--state` | Enum formatting: `fn` (default), `fqn`, `sn` |
|
|
||||||
| `--debug` | Verbose analysis logging and unresolved chain diagnostics |
|
|
||||||
| `--no-diamonds` | Render choice/junction states as regular nodes (default: diamonds) |
|
|
||||||
| `--include-generated-sources` | Also scan `target/` / `build/` generated `.java` trees (default: on) |
|
|
||||||
| `--no-inline-accessors` | Disable accessor indexing/inlining (enabled by default) |
|
|
||||||
|
|
||||||
### 2. Generate Interactive Portal (HTML)
|
### 2. Generate Interactive Portal (HTML)
|
||||||
```bash
|
```bash
|
||||||
./gradlew :state_machine_exporter_html:run --args="-i ./my-project -o ./out_html"
|
./gradlew :state_machine_exporter_html:run --args="-i ./my-project -o ./out_html"
|
||||||
```
|
```
|
||||||
|
*Supports `--profiles prod` to resolve Spring property placeholders.*
|
||||||
HTML-specific options:
|
|
||||||
|
|
||||||
| Flag | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `-f, --flows` | Custom `flows.json` path |
|
|
||||||
| `-m, --machine` | Export only machines whose name contains this filter |
|
|
||||||
| `--open` | Open the generated portal in the default browser |
|
|
||||||
| `--no-metadata-pane` | Hide the left metadata pane (entry points, flows) |
|
|
||||||
| `--no-diamonds` | Render choice pseudostates as normal nodes |
|
|
||||||
| `--debug` | Same diagnostic mode as the core exporter |
|
|
||||||
| `--include-generated-sources` | Same generated-source scanning as the core exporter |
|
|
||||||
| `--no-inline-accessors` | Disable accessor indexing/inlining (enabled by default) |
|
|
||||||
| `-p, --profiles` | Spring profiles for property resolution |
|
|
||||||
|
|
||||||
## Business Flows
|
## Business Flows
|
||||||
Define sequence of events in `src/main/resources/flows.json`:
|
Define sequence of events in `src/main/resources/flows.json`:
|
||||||
@@ -56,32 +26,13 @@ Define sequence of events in `src/main/resources/flows.json`:
|
|||||||
{
|
{
|
||||||
"name": "Order Success",
|
"name": "Order Success",
|
||||||
"description": "Happy path for order placement",
|
"description": "Happy path for order placement",
|
||||||
"steps": [
|
"steps": ["PAY", "CHECK_AVAILABILITY->PENDING", "SHIP"]
|
||||||
"PAY",
|
|
||||||
"CHECK_AVAILABILITY->PENDING",
|
|
||||||
{ "source": "com.example.order.OrderState.PAID", "event": "com.example.order.OrderEvent.SHIP" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Event-only strings remain supported but do not highlight in the HTML explorer unless paired with a source state. Use `{source, event}` objects for precise transition highlighting.
|
|
||||||
|
|
||||||
## JSON Structure
|
## JSON Structure
|
||||||
- `metadata.entryPoints`: REST, WebFlux, and JMS entry points.
|
- `metadata.entryPoints`: REST, WebFlux, and JMS entry points.
|
||||||
- `metadata.callChains`: Trace from API call to machine trigger (`sendEvent`).
|
- `metadata.callChains`: Trace from API call to machine trigger (`sendEvent`).
|
||||||
- `transitions.actions[].internalLogic`: Extracted source code/lambdas.
|
- `transitions.actions[].internalLogic`: Extracted source code/lambdas.
|
||||||
- `metadata.properties`: Dictionary of all detected Spring profiles.
|
- `metadata.properties`: Dictionary of all detected Spring profiles.
|
||||||
|
|
||||||
## Static analysis: accessor inlining
|
|
||||||
|
|
||||||
During scan, the exporter builds an **accessor index** of trivial getters, setters, record components, and constant-returning methods. At analysis time, `AccessorResolver` uses this index first (O(1) lookup), then falls back to constructor/field tracing and method-body evaluation.
|
|
||||||
|
|
||||||
This improves resolution of DTO/event chains such as `request.getPayload().getType()` without running Maven or Gradle on the target project.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enabled by default; disable for debugging or A/B comparison
|
|
||||||
./gradlew :state_machine_exporter:run --args="-i ./my-project -o ./out --no-inline-accessors"
|
|
||||||
```
|
|
||||||
|
|
||||||
Pipeline components live under `analysis/pipeline/` (`AccessorResolver`, `ResolutionBudget`, `MethodInvocationUnwrapper`, `FieldInitializerFinder`, `LibraryUnwrapRegistry`).
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,17 +0,0 @@
|
|||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class TestHeuristic {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
System.out.println(isFullyQualifiedMethod("TransformerImpl.transform"));
|
|
||||||
System.out.println(isFullyQualifiedMethod("Transformer.transform"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
plan-extended-anaylis/PLAN.md
Normal file
71
plan-extended-anaylis/PLAN.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Execution Plan for Extended Analysis (Revised for Robustness)
|
||||||
|
|
||||||
|
## Phase 0: Surgical Refactoring (The Enrichment Hook)
|
||||||
|
- [x] Create `AnalysisResult` and `CodebaseMetadata` models.
|
||||||
|
- [x] Update `ExportService.generateOutputs` to accept `AnalysisResult`.
|
||||||
|
- [x] Implement `EnrichmentService` and `AnalysisEnricher` interface.
|
||||||
|
- [x] Wrap current parser outputs in `AnalysisResult` within `ExportService`.
|
||||||
|
|
||||||
|
## Phase 1: Foundation & Semantic Core
|
||||||
|
- [ ] **JDT Binding Resolution**: Configure `ASTParser` with project classpath and `setResolveBindings(true)`.
|
||||||
|
- [ ] **Workspace Scoping**: Implement scanner with configurable exclude patterns (`test/`, `build/`, `node_modules/`).
|
||||||
|
- [ ] **ConstantResolver**: Implement lookup for `public static final` constants across class boundaries using JDT bindings.
|
||||||
|
- [ ] **Basic PropertyResolver**: Scan `application.properties/yml` for simple key-value pairs.
|
||||||
|
- [ ] Implement `JdtIntelligenceProvider` as the primary semantic analyzer.
|
||||||
|
|
||||||
|
## Phase 2: Trigger Discovery & Instance Identity
|
||||||
|
- [ ] **GenericEventDetector**: Find `sendEvent` calls using `ASTVisitor`.
|
||||||
|
- [ ] **InstanceIdentifier**: Detect which State Machine is being targeted (via `@Qualifier`, bean names, or generic types `StateMachine<S, E>`).
|
||||||
|
- [ ] **Trigger Filter**: Link `sendEvent` calls to specific SM instances to avoid noise in multi-SM projects.
|
||||||
|
- [ ] Enhance `CodebaseContext` to provide mapping from `MethodDeclaration` to its callers using bindings.
|
||||||
|
|
||||||
|
## Phase 3: Entry Point Mapping
|
||||||
|
- [ ] **SpringMvcDetector**: Detect REST endpoints (using `ConstantResolver` for paths).
|
||||||
|
- [ ] **MessageListenerDetector**: Detect Kafka/JMS listeners (using `ConstantResolver` for topics/queues).
|
||||||
|
- [ ] **ConfigurableTriggerDetector**: Support user-defined patterns for custom frameworks.
|
||||||
|
|
||||||
|
## Phase 4: Call Graph Integration
|
||||||
|
- [x] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls.
|
||||||
|
- [ ] Support inheritance in call graph resolution (using JDT bindings to resolve method overrides).
|
||||||
|
|
||||||
|
## Phase 4.5: Hierarchical Robustness (Inheritance Support)
|
||||||
|
- [ ] **Hierarchical Annotation Lookup**: Update `SpringMvcDetector` to scan interfaces and superclasses for inherited mapping annotations.
|
||||||
|
- [ ] **Interface Implementation Mapping**: Enhance `CodebaseContext` to index "Class -> Interfaces" and "Interface -> Classes" relationships.
|
||||||
|
- [ ] **Polymorphic Call Trace**: Update `CallGraphBuilder` to follow calls from an interface method to all known implementations in the project.
|
||||||
|
- [ ] **Base Class Trigger Discovery**: Ensure triggers in abstract base classes are correctly attributed to their concrete subclasses.
|
||||||
|
|
||||||
|
|
||||||
|
## Phase 5: Visualization & Diagnostics
|
||||||
|
- [ ] Update `ExportService` to include trigger information.
|
||||||
|
- [ ] Update DOT/SCXML exporters to show entry points.
|
||||||
|
- [ ] **Diagnostic Nodes**: Show "Potential/Unresolved Trigger" nodes when a `sendEvent` is found but the event name or SM instance cannot be resolved.
|
||||||
|
|
||||||
|
## Phase 6: Validation (Milestone 1)
|
||||||
|
- [ ] Create a "Complex Sample Project" with REST controllers, services, and multiple state machines.
|
||||||
|
- [ ] Add unit tests for each detector and the aggregator.
|
||||||
|
- [ ] Verify that inheritance in both state machine config and controllers is correctly handled.
|
||||||
|
|
||||||
|
## Phase 7: Advanced Resolution & Data Flow
|
||||||
|
- [ ] **Variable Propagation**: Track values from fields/constructors to their usage in the State Machine configuration (e.g., resolving `initial(initialState)` where `initialState` is a field).
|
||||||
|
- [ ] **@Value Resolution**: Link `@Value` annotations to the `PropertyResolver` to substitute placeholders with literal values in the diagram.
|
||||||
|
- [ ] **Spring Profile Support**: Update `PropertyResolver` to handle `application-{profile}.properties` and prioritize them based on an "active profiles" setting.
|
||||||
|
- [ ] **Complex ValueResolver**: Handle string concatenations (e.g., `"PREFIX_" + MyConstants.SUFFIX`) and variable references.
|
||||||
|
- [ ] **Payload Analysis**: Extract payload types from entry points and track field usage in `sendEvent` calls.
|
||||||
|
|
||||||
|
## Phase 8: Persistence & Lifecycle Mapping
|
||||||
|
- [ ] Detect persistence restoration logic (`persister.restore`).
|
||||||
|
- [ ] Map "Resume" points where the machine state is loaded from a DB based on external identifiers.
|
||||||
|
- [ ] Update metadata to distinguish between "New Instance" and "Restored Instance" triggers.
|
||||||
|
|
||||||
|
## Phase 9: Advanced Ecosystem Support
|
||||||
|
- [ ] **Reactive Chains**: Analyze WebFlux/Project Reactor lambdas (`doOnNext`, `flatMap`) for hidden triggers.
|
||||||
|
- [ ] **Interceptors & Filters**: Detect `HandlerInterceptor` and `Filter` implementations and map them to endpoints.
|
||||||
|
|
||||||
|
## Phase 10: External Dependency Interop
|
||||||
|
- [x] **Library Hints**: Implementation complete. Supports `hints.json` in project root or `src/main/resources/`.
|
||||||
|
- **Format**: `[{"methodFqn": "class.method", "event": "EVENT"}]`
|
||||||
|
- **Purpose**: Maps external/compiled library calls to State Machine events when source code is unavailable.
|
||||||
|
- [ ] **External JAR Analysis**: (Optional) Configure JDT to scan source-attachments of dependencies if available.
|
||||||
|
|
||||||
|
**Note on Library Hints**: This feature is an optional "Power User" escape hatch. The analyzer works automatically for all local source code; `hints.json` is only required to bridge the gap for third-party libraries.
|
||||||
|
|
||||||
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.
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
task runGolden(type: JavaExec) {
|
task runGolden(type: JavaExec) {
|
||||||
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
||||||
classpath = sourceSets.test.runtimeClasspath
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
workingDir = rootProject.projectDir
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ 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:extended_analysis_sample'
|
||||||
include ':state_machines:inheritance_sample'
|
include ':state_machines:inheritance_sample'
|
||||||
include ':state_machines:layered_dispatcher_sample'
|
|
||||||
include ':state_machines:ultimate_ecosystem_sm'
|
include ':state_machines:ultimate_ecosystem_sm'
|
||||||
include ':state_machines:enterprise_order_system'
|
include ':state_machines:enterprise_order_system'
|
||||||
include ':state_machines:multi_module_sample:api-module'
|
include ':state_machines:multi_module_sample:api-module'
|
||||||
include ':state_machines:multi_module_sample:core-module'
|
include ':state_machines:multi_module_sample:core-module'
|
||||||
include ':state_machines:state_machine_enterprise'
|
|
||||||
|
|||||||
@@ -48,20 +48,20 @@ dependencies {
|
|||||||
testImplementation 'org.assertj:assertj-core:3.27.7'
|
testImplementation 'org.assertj:assertj-core:3.27.7'
|
||||||
|
|
||||||
implementation 'net.sourceforge.plantuml:plantuml:1.2024.3'
|
implementation 'net.sourceforge.plantuml:plantuml:1.2024.3'
|
||||||
|
|
||||||
// Library types for JDT binding when analyzing cloned source (never runs the target project's build).
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter:3.2.0'
|
|
||||||
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
maxParallelForks = Math.max(1, Runtime.runtime.availableProcessors().intdiv(2) ?: 1)
|
|
||||||
systemProperty "updateGolden", System.getProperty("updateGolden")
|
systemProperty "updateGolden", System.getProperty("updateGolden")
|
||||||
}
|
}
|
||||||
|
|
||||||
task runGolden(type: JavaExec) {
|
task runGolden(type: JavaExec) {
|
||||||
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
||||||
classpath = sourceSets.test.runtimeClasspath
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
|
}
|
||||||
|
|
||||||
|
task runGoldenFixed(type: JavaExec) {
|
||||||
|
mainClass = "click.kamil.springstatemachineexporter.GoldenUpdater"
|
||||||
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
workingDir = rootProject.projectDir
|
workingDir = rootProject.projectDir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Deep audit of golden JSON analysis exports."""
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
GOLDEN = Path(__file__).resolve().parent.parent / "src/test/resources/golden"
|
|
||||||
PH = re.compile(r"\$\{")
|
|
||||||
|
|
||||||
|
|
||||||
def fn_form(identifier):
|
|
||||||
if not identifier:
|
|
||||||
return identifier
|
|
||||||
last = identifier.rfind(".")
|
|
||||||
if last <= 0:
|
|
||||||
return identifier
|
|
||||||
prev = identifier.rfind(".", 0, last)
|
|
||||||
return identifier[prev + 1 :] if prev > 0 else identifier
|
|
||||||
|
|
||||||
|
|
||||||
def sid(st):
|
|
||||||
if isinstance(st, dict):
|
|
||||||
return st.get("fullIdentifier") or st.get("rawName")
|
|
||||||
return st
|
|
||||||
|
|
||||||
|
|
||||||
def eid(ev):
|
|
||||||
if isinstance(ev, dict):
|
|
||||||
return ev.get("fullIdentifier") or ev.get("rawName")
|
|
||||||
return ev
|
|
||||||
|
|
||||||
|
|
||||||
def audit_file(path, data):
|
|
||||||
name = data.get("name", path.stem)
|
|
||||||
issues = []
|
|
||||||
st_fqn = data.get("stateTypeFqn")
|
|
||||||
ev_fqn = data.get("eventTypeFqn")
|
|
||||||
transitions = data.get("transitions") or []
|
|
||||||
starts = list(data.get("startStates") or [])
|
|
||||||
ends = list(data.get("endStates") or [])
|
|
||||||
states = data.get("states") or []
|
|
||||||
meta = data.get("metadata") or {}
|
|
||||||
|
|
||||||
is_pkg_enum = st_fqn and "." in st_fqn and st_fqn not in ("String", "java.lang.String")
|
|
||||||
is_string = st_fqn in (None, "String", "java.lang.String") or (
|
|
||||||
not st_fqn and not ev_fqn
|
|
||||||
)
|
|
||||||
|
|
||||||
if PH.search(json.dumps(data)):
|
|
||||||
issues.append("BUG: unresolved ${placeholder} in export")
|
|
||||||
|
|
||||||
# Duplicate fullIdentifiers in states[]
|
|
||||||
by_full = defaultdict(list)
|
|
||||||
for s in states:
|
|
||||||
by_full[sid(s)].append(s.get("rawName"))
|
|
||||||
for fid, raws in by_full.items():
|
|
||||||
if fid and len(raws) > 1 and len(set(raws)) > 1:
|
|
||||||
issues.append(f"NOISE: duplicate state entry {fid!r} with rawNames {raws}")
|
|
||||||
|
|
||||||
# Collect transition state/event ids
|
|
||||||
t_events, t_sources, t_targets = set(), set(), set()
|
|
||||||
for t in transitions:
|
|
||||||
if eid(t.get("event")):
|
|
||||||
t_events.add(eid(t.get("event")))
|
|
||||||
for s in t.get("sourceStates") or []:
|
|
||||||
t_sources.add(sid(s))
|
|
||||||
for s in t.get("targetStates") or []:
|
|
||||||
t_targets.add(sid(s))
|
|
||||||
|
|
||||||
graph = t_sources | t_targets
|
|
||||||
|
|
||||||
# startStates canonical consistency
|
|
||||||
for ss in starts:
|
|
||||||
if is_string and ss and not ss.startswith("String."):
|
|
||||||
if any(x.startswith("String.") for x in graph):
|
|
||||||
issues.append(f"BUG: startStates {ss!r} bare but transitions use String.* form")
|
|
||||||
if ss not in graph and fn_form(ss) not in {fn_form(x) for x in graph}:
|
|
||||||
issues.append(f"WARN: startStates {ss!r} not in transition graph")
|
|
||||||
|
|
||||||
# endStates
|
|
||||||
for es in ends:
|
|
||||||
if es not in graph and fn_form(es) not in {fn_form(x) for x in graph}:
|
|
||||||
issues.append(f"WARN: endStates {es!r} not in transition graph")
|
|
||||||
|
|
||||||
# Enum canonical fullIdentifiers
|
|
||||||
if is_pkg_enum:
|
|
||||||
for i, t in enumerate(transitions):
|
|
||||||
for side in ("sourceStates", "targetStates"):
|
|
||||||
for s in t.get(side) or []:
|
|
||||||
fid = sid(s)
|
|
||||||
if fid and not fid.startswith(st_fqn + ".") and not fid.startswith('"'):
|
|
||||||
if "." in fid and not fid.startswith("<"):
|
|
||||||
issues.append(f"BUG: transitions[{i}].{side} {fid!r} not under {st_fqn}")
|
|
||||||
ev = eid(t.get("event"))
|
|
||||||
if ev and ev_fqn and "." in ev_fqn and not ev.startswith("<") and not ev.startswith(ev_fqn + "."):
|
|
||||||
if ev not in ("event",) and "." in ev and not ev.startswith(ev_fqn):
|
|
||||||
issues.append(f"BUG: transitions[{i}].event {ev!r} not under {ev_fqn}")
|
|
||||||
for i, tr in enumerate(meta.get("triggers") or []):
|
|
||||||
for field in ("event", "sourceState"):
|
|
||||||
val = tr.get(field)
|
|
||||||
if not val or val in ("event", "payload", "customMessage", "eventProvider"):
|
|
||||||
continue
|
|
||||||
type_fqn = ev_fqn if field == "event" else st_fqn
|
|
||||||
if type_fqn and "." in val and not val.startswith("<") and not val.startswith(type_fqn + "."):
|
|
||||||
if field == "sourceState" and val not in graph and fn_form(val) not in {fn_form(x) for x in graph}:
|
|
||||||
issues.append(f"BUG: triggers[{i}].sourceState {val!r} not a machine state")
|
|
||||||
elif field == "event" and type_fqn and val.isupper() is False and "." not in val and not val.startswith("<"):
|
|
||||||
if val not in ("event", "payload", "customMessage", "eventProvider", "true", "false"):
|
|
||||||
issues.append(f"INFO: triggers[{i}].event dynamic {val!r}")
|
|
||||||
|
|
||||||
# matchedTransitions
|
|
||||||
for i, chain in enumerate(meta.get("callChains") or []):
|
|
||||||
for j, mt in enumerate(chain.get("matchedTransitions") or []):
|
|
||||||
me, ms, mtgt = mt.get("event"), mt.get("sourceState"), mt.get("targetState")
|
|
||||||
ok = False
|
|
||||||
for t in transitions:
|
|
||||||
if fn_form(eid(t.get("event"))) != fn_form(me):
|
|
||||||
continue
|
|
||||||
srcs = [sid(s) for s in t.get("sourceStates") or []]
|
|
||||||
tgts = [sid(s) for s in t.get("targetStates") or []]
|
|
||||||
if (not ms or any(fn_form(x) == fn_form(ms) for x in srcs)) and (
|
|
||||||
not mtgt or any(fn_form(x) == fn_form(mtgt) for x in tgts)
|
|
||||||
):
|
|
||||||
ok = True
|
|
||||||
break
|
|
||||||
if not ok:
|
|
||||||
issues.append(
|
|
||||||
f"BUG: callChains[{i}].matchedTransitions[{j}] ({ms!r},{me!r},{mtgt!r}) no transition"
|
|
||||||
)
|
|
||||||
|
|
||||||
# String machine: mixed NEW vs String.NEW
|
|
||||||
if is_string or st_fqn == "String":
|
|
||||||
all_ids = set(graph) | set(starts) | set(ends) | {sid(s) for s in states}
|
|
||||||
for x in list(all_ids):
|
|
||||||
if not x:
|
|
||||||
continue
|
|
||||||
bare = x.strip('"')
|
|
||||||
if bare and "." not in bare and f"String.{bare}" in all_ids:
|
|
||||||
issues.append(f"BUG: mixed bare {x!r} and String.{bare}")
|
|
||||||
|
|
||||||
# stateTypeFqn missing on enum machine
|
|
||||||
if is_pkg_enum and not st_fqn:
|
|
||||||
issues.append("BUG: enum machine missing stateTypeFqn")
|
|
||||||
|
|
||||||
return name, issues
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
total = 0
|
|
||||||
for path in sorted(GOLDEN.rglob("*.json")):
|
|
||||||
data = json.loads(path.read_text())
|
|
||||||
machine, issues = audit_file(path, data)
|
|
||||||
rel = path.relative_to(GOLDEN)
|
|
||||||
bugs = [i for i in issues if i.startswith("BUG")]
|
|
||||||
warns = [i for i in issues if i.startswith("WARN")]
|
|
||||||
noise = [i for i in issues if i.startswith("NOISE")]
|
|
||||||
info = [i for i in issues if i.startswith("INFO")]
|
|
||||||
if bugs or warns:
|
|
||||||
print(f"\n{'='*70}\n{rel}\n machine: {machine}")
|
|
||||||
for i in bugs + warns + noise + info:
|
|
||||||
print(f" [{i.split(':')[0]}] {i.split(': ', 1)[-1]}")
|
|
||||||
total += len(bugs) + len(warns)
|
|
||||||
print(f"\nTotal BUG+WARN: {total}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Final enrichment step: fail fast when enum identifiers in the analysis model are not
|
|
||||||
* package-canonical. Disable with {@code -Danalyzer.canonical-form-validation.enabled=false}.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class AnalysisCanonicalFormEnricher implements AnalysisEnricher {
|
|
||||||
|
|
||||||
private final boolean enabled;
|
|
||||||
|
|
||||||
public AnalysisCanonicalFormEnricher() {
|
|
||||||
this.enabled = Boolean.parseBoolean(
|
|
||||||
System.getProperty("analyzer.canonical-form-validation.enabled", "true"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
|
||||||
if (!enabled || result == null || context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.debug("Validating canonical enum identifiers for {}", result.getName());
|
|
||||||
AnalysisCanonicalFormValidator.enforce(result, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,16 +3,11 @@ package click.kamil.springstatemachineexporter.analysis.enricher;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CallChainEnricher implements AnalysisEnricher {
|
public class CallChainEnricher implements AnalysisEnricher {
|
||||||
@@ -20,88 +15,14 @@ public class CallChainEnricher implements AnalysisEnricher {
|
|||||||
@Override
|
@Override
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
log.info("Enriching {} with call chains", result.getName());
|
log.info("Enriching {} with call chains", result.getName());
|
||||||
List<CallChain> chains = buildCallChainsForMachine(result, context, intelligence, true);
|
|
||||||
if (chains == null) {
|
List<CallChain> chains = intelligence.findCallChains(
|
||||||
return;
|
result.getMetadata().getEntryPoints(),
|
||||||
}
|
result.getMetadata().getTriggers()
|
||||||
|
);
|
||||||
|
|
||||||
result.addMetadata(CodebaseMetadata.builder()
|
result.addMetadata(CodebaseMetadata.builder()
|
||||||
.callChains(chains)
|
.callChains(chains)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-resolves call chains from the codebase call graph for the given machine.
|
|
||||||
* Used on JSON re-export when source is available to refresh {@code polymorphicEvents},
|
|
||||||
* {@code external}, and branch {@code constraint} on trigger points.
|
|
||||||
*/
|
|
||||||
public static List<CallChain> buildCallChainsForMachine(
|
|
||||||
AnalysisResult result,
|
|
||||||
CodebaseContext context,
|
|
||||||
CodebaseIntelligenceProvider intelligence) {
|
|
||||||
return buildCallChainsForMachine(result, context, intelligence, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CallChain> buildCallChainsForMachine(
|
|
||||||
AnalysisResult result,
|
|
||||||
CodebaseContext context,
|
|
||||||
CodebaseIntelligenceProvider intelligence,
|
|
||||||
boolean preferMetadataTriggers) {
|
|
||||||
if (result.getMetadata() == null || intelligence == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<EntryPoint> entryPoints = result.getMetadata().getEntryPoints();
|
|
||||||
if (entryPoints == null || entryPoints.isEmpty()) {
|
|
||||||
entryPoints = synthesizeEntryPoints(result.getMetadata().getCallChains());
|
|
||||||
}
|
|
||||||
if (entryPoints == null || entryPoints.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TriggerPoint> scopedTriggers = null;
|
|
||||||
if (preferMetadataTriggers) {
|
|
||||||
scopedTriggers = result.getMetadata().getTriggers();
|
|
||||||
}
|
|
||||||
if (scopedTriggers == null || scopedTriggers.isEmpty()) {
|
|
||||||
scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
|
|
||||||
intelligence.findTriggerPoints(),
|
|
||||||
result.getName(),
|
|
||||||
context);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CallChain> chains = intelligence.findCallChains(entryPoints, scopedTriggers);
|
|
||||||
return MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<EntryPoint> synthesizeEntryPoints(List<CallChain> callChains) {
|
|
||||||
if (callChains == null || callChains.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
Map<String, EntryPoint> byMethod = new LinkedHashMap<>();
|
|
||||||
for (CallChain chain : callChains) {
|
|
||||||
EntryPoint entryPoint = chain.getEntryPoint();
|
|
||||||
if (hasClassAndMethod(entryPoint)) {
|
|
||||||
byMethod.putIfAbsent(entryPoint.getClassName() + "#" + entryPoint.getMethodName(), entryPoint);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
TriggerPoint triggerPoint = chain.getTriggerPoint();
|
|
||||||
if (triggerPoint != null
|
|
||||||
&& triggerPoint.getClassName() != null
|
|
||||||
&& triggerPoint.getMethodName() != null) {
|
|
||||||
EntryPoint fallback = EntryPoint.builder()
|
|
||||||
.type(EntryPoint.Type.CUSTOM)
|
|
||||||
.className(triggerPoint.getClassName())
|
|
||||||
.methodName(triggerPoint.getMethodName())
|
|
||||||
.sourceFile(triggerPoint.getSourceFile())
|
|
||||||
.build();
|
|
||||||
byMethod.putIfAbsent(fallback.getClassName() + "#" + fallback.getMethodName(), fallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new ArrayList<>(byMethod.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasClassAndMethod(EntryPoint entryPoint) {
|
|
||||||
return entryPoint != null
|
|
||||||
&& entryPoint.getClassName() != null
|
|
||||||
&& entryPoint.getMethodName() != null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ExternalTriggerPolicy;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Central rules for transition linking outcomes derived from existing trigger evidence only.
|
|
||||||
*/
|
|
||||||
public final class CallChainLinkPolicy {
|
|
||||||
|
|
||||||
private CallChainLinkPolicy() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the machine event enum FQN from config AST, embedded analysis fields, or transition events.
|
|
||||||
* Abstract/inherited configs often omit AST type args; transition {@code fullIdentifier} values still
|
|
||||||
* carry the correct enum type for endpoint narrowing and linking.
|
|
||||||
*/
|
|
||||||
public static String resolveMachineEventTypeFqn(
|
|
||||||
String machineConfigName,
|
|
||||||
String embeddedEventTypeFqn,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
List<Transition> machineTransitions,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (machineTypes != null && machineTypes.eventTypeFqn() != null && !machineTypes.eventTypeFqn().isBlank()) {
|
|
||||||
return machineTypes.eventTypeFqn();
|
|
||||||
}
|
|
||||||
if (embeddedEventTypeFqn != null && !embeddedEventTypeFqn.isBlank()) {
|
|
||||||
return embeddedEventTypeFqn;
|
|
||||||
}
|
|
||||||
if (context != null && machineConfigName != null) {
|
|
||||||
String[] types = StateMachineTypeResolver.resolve(machineConfigName, context);
|
|
||||||
if (types != null && types.length > 1 && types[1] != null && !types[1].isBlank()) {
|
|
||||||
return types[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return eventTypeFromTransitions(machineTransitions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StateMachineTypeResolver.MachineTypes resolveEffectiveMachineTypes(
|
|
||||||
String machineConfigName,
|
|
||||||
String embeddedStateTypeFqn,
|
|
||||||
String embeddedEventTypeFqn,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
List<Transition> machineTransitions,
|
|
||||||
CodebaseContext context) {
|
|
||||||
String eventTypeFqn = resolveMachineEventTypeFqn(
|
|
||||||
machineConfigName, embeddedEventTypeFqn, machineTypes, machineTransitions, context);
|
|
||||||
String stateTypeFqn = machineTypes != null && machineTypes.stateTypeFqn() != null
|
|
||||||
? machineTypes.stateTypeFqn()
|
|
||||||
: embeddedStateTypeFqn;
|
|
||||||
if ((stateTypeFqn == null || stateTypeFqn.isBlank()) && context != null && machineConfigName != null) {
|
|
||||||
String[] types = StateMachineTypeResolver.resolve(machineConfigName, context);
|
|
||||||
if (types != null && types.length > 0 && types[0] != null && !types[0].isBlank()) {
|
|
||||||
stateTypeFqn = types[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((stateTypeFqn == null || stateTypeFqn.isBlank()) && machineTransitions != null) {
|
|
||||||
stateTypeFqn = stateTypeFromTransitions(machineTransitions);
|
|
||||||
}
|
|
||||||
return new StateMachineTypeResolver.MachineTypes(stateTypeFqn, eventTypeFqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String eventTypeFromTransitions(List<Transition> machineTransitions) {
|
|
||||||
if (machineTransitions == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (Transition transition : machineTransitions) {
|
|
||||||
Event event = transition.getEvent();
|
|
||||||
if (event == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String fullIdentifier = event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
|
|
||||||
if (fullIdentifier != null && fullIdentifier.contains(".")) {
|
|
||||||
return fullIdentifier.substring(0, fullIdentifier.lastIndexOf('.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stateTypeFromTransitions(List<Transition> machineTransitions) {
|
|
||||||
if (machineTransitions == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (Transition transition : machineTransitions) {
|
|
||||||
if (transition.getSourceStates() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (var state : transition.getSourceStates()) {
|
|
||||||
String fullIdentifier = state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName();
|
|
||||||
if (fullIdentifier != null && fullIdentifier.contains(".")) {
|
|
||||||
return fullIdentifier.substring(0, fullIdentifier.lastIndexOf('.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldFailClosedOnAmbiguousCallGraphWiden(TriggerPoint trigger) {
|
|
||||||
return shouldFailClosedOnAmbiguousCallGraphWiden(trigger, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldFailClosedOnAmbiguousCallGraphWiden(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
String machineEventTypeFqn) {
|
|
||||||
return shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldFailClosedOnAmbiguousCallGraphWiden(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
String machineEventTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (trigger == null || trigger.isExternal() || !trigger.isAmbiguous()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String eventTypeFqn = trigger.getEventTypeFqn();
|
|
||||||
if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
|
|
||||||
eventTypeFqn = machineEventTypeFqn;
|
|
||||||
}
|
|
||||||
List<String> concrete = concretePolymorphicCandidates(
|
|
||||||
trigger.getPolymorphicEvents(), eventTypeFqn, context);
|
|
||||||
if (concrete.size() <= 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String event = trigger.getEvent();
|
|
||||||
if (event != null && event.startsWith("ENUM_SET:")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return MachineEnumCanonicalizer.isDynamicTriggerExpression(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True when call-graph evidence resolves to a single concrete machine enum event for this chain.
|
|
||||||
* Dispatcher/rich-event paths should narrow to one constant per endpoint before linking.
|
|
||||||
*/
|
|
||||||
public static boolean isEndpointNarrowPolyEvidence(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
String machineEventTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (trigger == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String eventTypeFqn = trigger.getEventTypeFqn();
|
|
||||||
if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
|
|
||||||
eventTypeFqn = machineEventTypeFqn;
|
|
||||||
}
|
|
||||||
List<String> concrete = concretePolymorphicCandidates(
|
|
||||||
trigger.getPolymorphicEvents(), eventTypeFqn, context);
|
|
||||||
return concrete.size() == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isEndpointNarrowPolyEvidence(TriggerPoint trigger) {
|
|
||||||
return isEndpointNarrowPolyEvidence(trigger, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isEndpointNarrowPolyEvidence(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
String machineEventTypeFqn) {
|
|
||||||
return isEndpointNarrowPolyEvidence(trigger, machineEventTypeFqn, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True when every concrete polymorphic candidate belongs to the trigger's declared event type.
|
|
||||||
* Used to allow machine-scoped symbolic expansion for generic dispatchers while rejecting
|
|
||||||
* cross-package or unqualified widens.
|
|
||||||
*/
|
|
||||||
static boolean isTrustedEnumPolymorphicWiden(TriggerPoint trigger) {
|
|
||||||
return isTrustedEnumPolymorphicWiden(trigger, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isTrustedEnumPolymorphicWiden(TriggerPoint trigger, String machineEventTypeFqn) {
|
|
||||||
return isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isTrustedEnumPolymorphicWiden(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
String machineEventTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (trigger == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String eventTypeFqn = trigger.getEventTypeFqn();
|
|
||||||
if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
|
|
||||||
eventTypeFqn = machineEventTypeFqn;
|
|
||||||
}
|
|
||||||
if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
List<String> poly = trigger.getPolymorphicEvents();
|
|
||||||
List<String> concrete = concretePolymorphicCandidates(poly, eventTypeFqn, context);
|
|
||||||
if (concrete.size() != 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return MachineEnumCanonicalizer.polymorphicEventMatchesMachineEventType(
|
|
||||||
concrete.get(0), eventTypeFqn, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<String> concretePolymorphicCandidates(
|
|
||||||
List<String> poly,
|
|
||||||
String eventTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (poly == null || poly.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> concrete = new ArrayList<>();
|
|
||||||
for (String pe : poly) {
|
|
||||||
if (pe == null || pe.startsWith("<SYMBOLIC:") || pe.startsWith("ENUM_SET:")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String qualified = pe;
|
|
||||||
if (!pe.contains(".")) {
|
|
||||||
if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
qualified = eventTypeFqn + "." + pe;
|
|
||||||
} else if (eventTypeFqn != null && !eventTypeFqn.isBlank()) {
|
|
||||||
qualified = MachineEnumCanonicalizer.canonicalizeLabel(pe, eventTypeFqn, context);
|
|
||||||
}
|
|
||||||
if (qualified != null && qualified.contains(".")) {
|
|
||||||
concrete.add(qualified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return concrete;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LinkResolution resolveLinkResolution(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
List<MatchedTransition> matched,
|
|
||||||
boolean ambiguousSource) {
|
|
||||||
return resolveLinkResolution(trigger, matched, ambiguousSource, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LinkResolution resolveLinkResolution(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
List<MatchedTransition> matched,
|
|
||||||
boolean ambiguousSource,
|
|
||||||
String machineEventTypeFqn) {
|
|
||||||
return resolveLinkResolution(trigger, matched, ambiguousSource, machineEventTypeFqn, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LinkResolution resolveLinkResolution(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
List<MatchedTransition> matched,
|
|
||||||
boolean ambiguousSource,
|
|
||||||
String machineEventTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (trigger != null && trigger.isExternal()) {
|
|
||||||
return LinkResolution.UNRESOLVED_EXTERNAL;
|
|
||||||
}
|
|
||||||
if (shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, context)) {
|
|
||||||
return LinkResolution.AMBIGUOUS_WIDEN;
|
|
||||||
}
|
|
||||||
if (ambiguousSource) {
|
|
||||||
return LinkResolution.AMBIGUOUS_WIDEN;
|
|
||||||
}
|
|
||||||
if (matched != null && !matched.isEmpty()) {
|
|
||||||
return LinkResolution.RESOLVED;
|
|
||||||
}
|
|
||||||
return LinkResolution.NO_MATCH;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TriggerPoint applyRestExternalPolicy(TriggerPoint trigger, CallChain chain, CodebaseContext context) {
|
|
||||||
if (trigger == null || chain.getEntryPoint() == null) {
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
EntryPoint entryPoint = chain.getEntryPoint();
|
|
||||||
String entryMethod = entryPoint.getClassName() + "." + entryPoint.getMethodName();
|
|
||||||
boolean external = ExternalTriggerPolicy.isExternalFromSource(
|
|
||||||
entryPoint, trigger, entryMethod, trigger.getEvent(), context);
|
|
||||||
if (external == trigger.isExternal()) {
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
return trigger.toBuilder().external(external).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,543 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.path.SymbolicPathValueEstimator;
|
|
||||||
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.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.QualifiedName;
|
|
||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
|
||||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Narrows wide call-graph polymorphic widens to endpoint-specific enum constants.
|
|
||||||
*
|
|
||||||
* <p>Uses multiple independent evidence sources (REST path, command keys, constraints, static trigger
|
|
||||||
* events, method-chain literals, camelCase method names) and only narrows when evidence converges.
|
|
||||||
*/
|
|
||||||
public final class CallChainPolyNarrower {
|
|
||||||
|
|
||||||
private static final int STRONG = 3;
|
|
||||||
private static final int MEDIUM = 2;
|
|
||||||
private static final int WEAK = 1;
|
|
||||||
|
|
||||||
private static final Pattern QUOTED_EQUALS_PARAM = Pattern.compile(
|
|
||||||
"\"([^\"]+)\"\\.equalsIgnoreCase\\((\\w+)\\)");
|
|
||||||
private static final Pattern PARAM_EQUALS_LITERAL = Pattern.compile(
|
|
||||||
"(\\w+)\\.equals(?:IgnoreCase)?\\(\"([^\"]+)\"\\)");
|
|
||||||
private static final Pattern ENUM_EQ = Pattern.compile(
|
|
||||||
"([A-Za-z_][\\w.]*)\\s*==\\s*([A-Za-z_][\\w.]*)");
|
|
||||||
private static final Pattern ROUTE_LITERAL = Pattern.compile(
|
|
||||||
"route\\(\\s*\"([^\"]+)\"\\s*,\\s*\"([^\"]+)\"\\s*\\)");
|
|
||||||
|
|
||||||
private static final Set<String> FIRE_METHOD_NAMES = Set.of(
|
|
||||||
"sendEvent", "fire", "fireEvent", "dispatchEvent", "send", "trigger");
|
|
||||||
|
|
||||||
private CallChainPolyNarrower() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TriggerPoint narrowForCallChain(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
CallChain chain,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
List<Transition> machineTransitions,
|
|
||||||
CodebaseContext context) {
|
|
||||||
return narrowForCallChain(trigger, chain, machineTypes, null, machineTransitions, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TriggerPoint narrowForCallChain(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
CallChain chain,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
String embeddedEventTypeFqn,
|
|
||||||
List<Transition> machineTransitions,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (trigger == null) {
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
List<String> poly = trigger.getPolymorphicEvents();
|
|
||||||
if (poly == null || poly.size() <= 1) {
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
String eventTypeFqn = CallChainLinkPolicy.resolveMachineEventTypeFqn(
|
|
||||||
null,
|
|
||||||
embeddedEventTypeFqn != null ? embeddedEventTypeFqn : trigger.getEventTypeFqn(),
|
|
||||||
machineTypes,
|
|
||||||
machineTransitions,
|
|
||||||
context);
|
|
||||||
if (eventTypeFqn == null || eventTypeFqn.isBlank()) {
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
Set<String> configuredConstants = configuredEventConstants(machineTransitions, eventTypeFqn, context);
|
|
||||||
if (configuredConstants.isEmpty()) {
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
HintVotes votes = new HintVotes();
|
|
||||||
collectHints(trigger, chain, configuredConstants, context, votes);
|
|
||||||
|
|
||||||
String chosen = votes.choose(configuredConstants);
|
|
||||||
if (chosen == null) {
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> narrowed = filterPolyToConstant(poly, chosen, eventTypeFqn, context);
|
|
||||||
if (narrowed.size() == 1) {
|
|
||||||
return trigger.toBuilder()
|
|
||||||
.polymorphicEvents(narrowed)
|
|
||||||
.ambiguous(false)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return trigger.toBuilder()
|
|
||||||
.polymorphicEvents(List.of(eventTypeFqn + "." + chosen))
|
|
||||||
.ambiguous(false)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectHints(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
CallChain chain,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
CodebaseContext context,
|
|
||||||
HintVotes votes) {
|
|
||||||
collectFromStaticTriggerEvent(trigger, configuredConstants, votes);
|
|
||||||
collectFromEntryPoint(chain != null ? chain.getEntryPoint() : null, configuredConstants, votes);
|
|
||||||
collectFromConstraint(trigger.getConstraint(), configuredConstants, votes);
|
|
||||||
if (chain != null) {
|
|
||||||
collectFromMethodChain(chain.getMethodChain(), configuredConstants, votes);
|
|
||||||
collectFromSendEventLiterals(chain.getMethodChain(), configuredConstants, context, votes);
|
|
||||||
collectFromPathEstimator(chain, configuredConstants, context, votes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFromStaticTriggerEvent(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
HintVotes votes) {
|
|
||||||
String event = trigger.getEvent();
|
|
||||||
if (event == null || event.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (MachineEnumCanonicalizer.isDynamicTriggerExpression(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addEnumReference(event, configuredConstants, votes, STRONG);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFromEntryPoint(
|
|
||||||
EntryPoint entryPoint,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
HintVotes votes) {
|
|
||||||
if (entryPoint == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (entryPoint.getName() != null) {
|
|
||||||
collectFromPath(entryPoint.getName(), configuredConstants, votes);
|
|
||||||
}
|
|
||||||
if (entryPoint.getMetadata() != null) {
|
|
||||||
Object path = entryPoint.getMetadata().get("path");
|
|
||||||
if (path instanceof String pathValue) {
|
|
||||||
collectFromPath(pathValue, configuredConstants, votes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (entryPoint.getMethodName() != null) {
|
|
||||||
addUniqueIdentifierMatch(entryPoint.getMethodName(), configuredConstants, votes, WEAK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFromPath(String rawPath, Set<String> configuredConstants, HintVotes votes) {
|
|
||||||
if (rawPath == null || rawPath.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int space = rawPath.indexOf(' ');
|
|
||||||
String path = space >= 0 ? rawPath.substring(space + 1).trim() : rawPath.trim();
|
|
||||||
if (path.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (String segment : path.split("/")) {
|
|
||||||
if (segment.isBlank() || segment.contains("{")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
addPathToken(segment, configuredConstants, votes, STRONG);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFromConstraint(
|
|
||||||
String constraint,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
HintVotes votes) {
|
|
||||||
if (constraint == null || constraint.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Matcher quoted = QUOTED_EQUALS_PARAM.matcher(constraint);
|
|
||||||
while (quoted.find()) {
|
|
||||||
String literal = quoted.group(1);
|
|
||||||
String param = quoted.group(2);
|
|
||||||
if ("event".equals(param) || "eventString".equals(param) || "actionKey".equals(param)
|
|
||||||
|| "action".equals(param)) {
|
|
||||||
addPathToken(literal, configuredConstants, votes, MEDIUM);
|
|
||||||
}
|
|
||||||
if ("commandKey".equals(param) || literal.contains(".")) {
|
|
||||||
addPathToken(literal, configuredConstants, votes, MEDIUM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Matcher route = ROUTE_LITERAL.matcher(constraint);
|
|
||||||
while (route.find()) {
|
|
||||||
addPathToken(route.group(2), configuredConstants, votes, MEDIUM);
|
|
||||||
}
|
|
||||||
Matcher enumEq = ENUM_EQ.matcher(constraint);
|
|
||||||
while (enumEq.find()) {
|
|
||||||
addEnumReference(enumEq.group(1), configuredConstants, votes, MEDIUM);
|
|
||||||
addEnumReference(enumEq.group(2), configuredConstants, votes, MEDIUM);
|
|
||||||
}
|
|
||||||
Matcher reverseEquals = PARAM_EQUALS_LITERAL.matcher(constraint);
|
|
||||||
while (reverseEquals.find()) {
|
|
||||||
String param = reverseEquals.group(1);
|
|
||||||
String literal = reverseEquals.group(2);
|
|
||||||
if ("event".equals(param) || "eventString".equals(param) || "actionKey".equals(param)
|
|
||||||
|| "action".equals(param) || "commandKey".equals(param)) {
|
|
||||||
addPathToken(literal, configuredConstants, votes, MEDIUM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFromMethodChain(
|
|
||||||
List<String> methodChain,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
HintVotes votes) {
|
|
||||||
if (methodChain == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (String methodFqn : methodChain) {
|
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
|
||||||
addUniqueIdentifierMatch(methodName, configuredConstants, votes, WEAK);
|
|
||||||
for (String token : splitCamelCase(methodName)) {
|
|
||||||
addPathToken(token, configuredConstants, votes, WEAK);
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
int nestedDot = className.lastIndexOf('.');
|
|
||||||
if (nestedDot >= 0) {
|
|
||||||
addUniqueIdentifierMatch(className.substring(nestedDot + 1), configuredConstants, votes, WEAK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFromSendEventLiterals(
|
|
||||||
List<String> methodChain,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
CodebaseContext context,
|
|
||||||
HintVotes votes) {
|
|
||||||
if (context == null || methodChain == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ConstantResolver resolver = new ConstantResolver();
|
|
||||||
for (String methodFqn : methodChain) {
|
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
|
||||||
if (methodDeclaration == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (Object parameter : methodDeclaration.parameters()) {
|
|
||||||
if (parameter instanceof SingleVariableDeclaration variable) {
|
|
||||||
addUniqueIdentifierMatch(variable.getType().toString(), configuredConstants, votes, MEDIUM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (methodDeclaration.getBody() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (!FIRE_METHOD_NAMES.contains(node.getName().getIdentifier())) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
for (Object argument : node.arguments()) {
|
|
||||||
if (!(argument instanceof Expression expression)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (expression instanceof QualifiedName qualifiedName) {
|
|
||||||
addEnumReference(qualifiedName.getFullyQualifiedName(), configuredConstants, votes, STRONG);
|
|
||||||
} else if (expression instanceof SimpleName simpleName) {
|
|
||||||
addEnumReference(simpleName.getIdentifier(), configuredConstants, votes, STRONG);
|
|
||||||
} else {
|
|
||||||
String resolved = resolver.resolve(expression, context);
|
|
||||||
if (resolved != null) {
|
|
||||||
addEnumReference(resolved, configuredConstants, votes, STRONG);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFromPathEstimator(
|
|
||||||
CallChain chain,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
CodebaseContext context,
|
|
||||||
HintVotes votes) {
|
|
||||||
if (context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<String> estimated = new SymbolicPathValueEstimator().estimatePossibleValues(chain, context);
|
|
||||||
if (estimated == null || estimated.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int weight = estimated.size() == 1 ? MEDIUM : WEAK;
|
|
||||||
for (String value : estimated) {
|
|
||||||
addEnumReference(value, configuredConstants, votes, weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addPathToken(
|
|
||||||
String token,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
HintVotes votes,
|
|
||||||
int weight) {
|
|
||||||
if (token == null || token.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addPathTokenVariants(token, configuredConstants, votes, weight);
|
|
||||||
if (token.contains(".")) {
|
|
||||||
addPathTokenVariants(token.substring(token.lastIndexOf('.') + 1), configuredConstants, votes, weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addPathTokenVariants(
|
|
||||||
String token,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
HintVotes votes,
|
|
||||||
int weight) {
|
|
||||||
maybeVoteConstant(votes, normalizeToken(token), configuredConstants, weight);
|
|
||||||
for (String part : token.split("[-_.]")) {
|
|
||||||
if (!part.isBlank()) {
|
|
||||||
maybeVoteConstant(votes, normalizeToken(part), configuredConstants, weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (token.toUpperCase(Locale.ROOT).contains("_")) {
|
|
||||||
String tail = token.substring(token.lastIndexOf('_') + 1);
|
|
||||||
maybeVoteConstant(votes, normalizeToken(tail), configuredConstants, weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addEnumReference(
|
|
||||||
String reference,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
HintVotes votes,
|
|
||||||
int weight) {
|
|
||||||
if (reference == null || reference.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
maybeVoteConstant(votes, constantName(reference), configuredConstants, weight);
|
|
||||||
if (reference.contains(".")) {
|
|
||||||
maybeVoteConstant(votes, constantName(reference), configuredConstants, weight);
|
|
||||||
}
|
|
||||||
String upper = reference.toUpperCase(Locale.ROOT);
|
|
||||||
if (upper.contains("_")) {
|
|
||||||
maybeVoteConstant(votes, upper.substring(upper.lastIndexOf('_') + 1), configuredConstants, weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addUniqueIdentifierMatch(
|
|
||||||
String identifier,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
HintVotes votes,
|
|
||||||
int weight) {
|
|
||||||
if (identifier == null || identifier.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String upper = identifier.toUpperCase(Locale.ROOT);
|
|
||||||
List<String> matched = new ArrayList<>();
|
|
||||||
for (String constant : configuredConstants) {
|
|
||||||
if (upper.contains(constant)) {
|
|
||||||
matched.add(constant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matched.size() == 1) {
|
|
||||||
votes.add(matched.get(0), weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void maybeVoteConstant(
|
|
||||||
HintVotes votes,
|
|
||||||
String candidate,
|
|
||||||
Set<String> configuredConstants,
|
|
||||||
int weight) {
|
|
||||||
if (candidate == null || candidate.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String normalized = candidate.toUpperCase(Locale.ROOT);
|
|
||||||
if (configuredConstants.contains(normalized)) {
|
|
||||||
votes.add(normalized, weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> filterPolyToConstant(
|
|
||||||
List<String> poly,
|
|
||||||
String constant,
|
|
||||||
String eventTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
List<String> narrowed = new ArrayList<>();
|
|
||||||
for (String candidate : poly) {
|
|
||||||
if (candidate == null || candidate.startsWith("<SYMBOLIC:") || candidate.startsWith("ENUM_SET:")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!constant.equalsIgnoreCase(constantName(candidate))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String canonical = candidate.contains(".")
|
|
||||||
? MachineEnumCanonicalizer.canonicalizeLabel(candidate, eventTypeFqn, context)
|
|
||||||
: eventTypeFqn + "." + constant;
|
|
||||||
if (!narrowed.contains(canonical)) {
|
|
||||||
narrowed.add(canonical);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return narrowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeToken(String token) {
|
|
||||||
return token.replace('-', '_').toUpperCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> splitCamelCase(String identifier) {
|
|
||||||
if (identifier == null || identifier.isBlank()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> tokens = new ArrayList<>();
|
|
||||||
StringBuilder current = new StringBuilder();
|
|
||||||
for (int i = 0; i < identifier.length(); i++) {
|
|
||||||
char ch = identifier.charAt(i);
|
|
||||||
if (Character.isUpperCase(ch) && !current.isEmpty()) {
|
|
||||||
tokens.add(current.toString());
|
|
||||||
current.setLength(0);
|
|
||||||
}
|
|
||||||
current.append(ch);
|
|
||||||
}
|
|
||||||
if (!current.isEmpty()) {
|
|
||||||
tokens.add(current.toString());
|
|
||||||
}
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> configuredEventConstants(
|
|
||||||
List<Transition> transitions,
|
|
||||||
String eventTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
Set<String> constants = new LinkedHashSet<>();
|
|
||||||
for (String event : MachineEnumCanonicalizer.polymorphicEventsFromTransitions(
|
|
||||||
transitions, eventTypeFqn, context)) {
|
|
||||||
constants.add(constantName(event).toUpperCase(Locale.ROOT));
|
|
||||||
}
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String constantName(String ref) {
|
|
||||||
int dot = ref.lastIndexOf('.');
|
|
||||||
return (dot >= 0 ? ref.substring(dot + 1) : ref).toUpperCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class HintVotes {
|
|
||||||
private final Map<String, Integer> scores = new HashMap<>();
|
|
||||||
private final Map<String, Integer> maxWeight = new HashMap<>();
|
|
||||||
|
|
||||||
void add(String constant, int weight) {
|
|
||||||
if (constant == null || constant.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String key = constant.toUpperCase(Locale.ROOT);
|
|
||||||
scores.merge(key, weight, Integer::sum);
|
|
||||||
maxWeight.merge(key, weight, Math::max);
|
|
||||||
}
|
|
||||||
|
|
||||||
String choose(Set<String> configured) {
|
|
||||||
String strongUnique = uniqueAboveWeight(configured, STRONG);
|
|
||||||
if (strongUnique != null) {
|
|
||||||
return strongUnique;
|
|
||||||
}
|
|
||||||
String mediumUnique = uniqueAboveWeight(configured, MEDIUM);
|
|
||||||
if (mediumUnique != null) {
|
|
||||||
return mediumUnique;
|
|
||||||
}
|
|
||||||
String best = null;
|
|
||||||
int bestScore = 0;
|
|
||||||
for (String constant : configured) {
|
|
||||||
int score = scores.getOrDefault(constant, 0);
|
|
||||||
if (score > bestScore) {
|
|
||||||
bestScore = score;
|
|
||||||
best = constant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (best == null || bestScore < MEDIUM) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final int winningScore = bestScore;
|
|
||||||
List<String> tied = new ArrayList<>();
|
|
||||||
for (String constant : configured) {
|
|
||||||
if (scores.getOrDefault(constant, 0) == winningScore) {
|
|
||||||
tied.add(constant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tied.size() == 1) {
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
String maxWeightWinner = null;
|
|
||||||
int bestMaxWeight = 0;
|
|
||||||
for (String constant : tied) {
|
|
||||||
int weight = maxWeight.getOrDefault(constant, 0);
|
|
||||||
if (weight > bestMaxWeight) {
|
|
||||||
bestMaxWeight = weight;
|
|
||||||
maxWeightWinner = constant;
|
|
||||||
} else if (weight == bestMaxWeight && weight > 0) {
|
|
||||||
maxWeightWinner = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return maxWeightWinner;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String uniqueAboveWeight(Set<String> configured, int minWeight) {
|
|
||||||
String chosen = null;
|
|
||||||
for (String constant : configured) {
|
|
||||||
if (maxWeight.getOrDefault(constant, 0) >= minWeight) {
|
|
||||||
if (chosen != null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
chosen = constant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chosen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,10 +16,8 @@ public class EntryPointEnricher implements AnalysisEnricher {
|
|||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
log.info("Enriching {} with entry points", result.getName());
|
log.info("Enriching {} with entry points", result.getName());
|
||||||
|
|
||||||
// Keep all entry points; machine scoping is applied after call-chain resolution
|
|
||||||
// when source evidence (trigger type FQNs, literals, constraints) is available.
|
|
||||||
List<EntryPoint> entryPoints = intelligence.findEntryPoints();
|
List<EntryPoint> entryPoints = intelligence.findEntryPoints();
|
||||||
|
|
||||||
result.addMetadata(CodebaseMetadata.builder()
|
result.addMetadata(CodebaseMetadata.builder()
|
||||||
.entryPoints(entryPoints)
|
.entryPoints(entryPoints)
|
||||||
.build());
|
.build());
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
|
||||||
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.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CallGraphEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.GenericEventDetector;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.JdtCallGraphEngine;
|
|
||||||
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 org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scopes REST entry points to a machine using call-graph reachability and trigger type evidence,
|
|
||||||
* not URL path segment guessing.
|
|
||||||
*/
|
|
||||||
public final class EntryPointScopeResolver {
|
|
||||||
|
|
||||||
private static final BeanResolutionEngine ROUTING = new HeuristicBeanResolutionEngine();
|
|
||||||
private static final String TRIGGERS_CACHE = "entryPointScope.triggers";
|
|
||||||
|
|
||||||
public enum Affinity {
|
|
||||||
FOR_MACHINE,
|
|
||||||
AGAINST_MACHINE,
|
|
||||||
UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
private EntryPointScopeResolver() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<EntryPoint> scopeFromCallChains(List<CallChain> chains, List<EntryPoint> allEntryPoints) {
|
|
||||||
Map<String, EntryPoint> scoped = new LinkedHashMap<>();
|
|
||||||
if (chains != null) {
|
|
||||||
for (CallChain chain : chains) {
|
|
||||||
EntryPoint entryPoint = chain.getEntryPoint();
|
|
||||||
if (entryPoint != null) {
|
|
||||||
scoped.putIfAbsent(entryPointKey(entryPoint), entryPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (allEntryPoints != null) {
|
|
||||||
for (EntryPoint entryPoint : allEntryPoints) {
|
|
||||||
if (hasPathVariablePlaceholder(entryPoint)) {
|
|
||||||
scoped.putIfAbsent(entryPointKey(entryPoint), entryPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new ArrayList<>(scoped.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Affinity resolveAffinity(EntryPoint entryPoint, String machineName, CodebaseContext context) {
|
|
||||||
if (entryPoint == null || machineName == null) {
|
|
||||||
return Affinity.UNKNOWN;
|
|
||||||
}
|
|
||||||
if (entryPoint.getClassName() == null || entryPoint.getMethodName() == null) {
|
|
||||||
return Affinity.UNKNOWN;
|
|
||||||
}
|
|
||||||
if (context == null || context.getCompilationUnits().isEmpty()) {
|
|
||||||
return Affinity.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
CallGraphEngine engine = createCallGraphEngine(context);
|
|
||||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), getOrCollectTriggers(context));
|
|
||||||
if (chains.isEmpty()) {
|
|
||||||
return Affinity.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean matched = false;
|
|
||||||
boolean mismatched = false;
|
|
||||||
for (CallChain chain : chains) {
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint();
|
|
||||||
if (trigger == null || !hasTypeEvidence(trigger)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ROUTING.isRoutedToCorrectMachine(chain, machineName, context)) {
|
|
||||||
matched = true;
|
|
||||||
} else {
|
|
||||||
mismatched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matched) {
|
|
||||||
return Affinity.FOR_MACHINE;
|
|
||||||
}
|
|
||||||
if (mismatched) {
|
|
||||||
return Affinity.AGAINST_MACHINE;
|
|
||||||
}
|
|
||||||
return Affinity.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasPathVariablePlaceholder(EntryPoint entryPoint) {
|
|
||||||
if (entryPoint == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String name = entryPoint.getName();
|
|
||||||
if (name != null && name.contains("{")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Map<String, String> metadata = entryPoint.getMetadata();
|
|
||||||
if (metadata != null) {
|
|
||||||
String path = metadata.get("path");
|
|
||||||
return path != null && path.contains("{");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasTypeEvidence(TriggerPoint trigger) {
|
|
||||||
if (trigger.getEventTypeFqn() != null || trigger.getStateTypeFqn() != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return MachineEnumCanonicalizer.classifyTriggerEvent(trigger.getEvent())
|
|
||||||
== MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static List<TriggerPoint> getOrCollectTriggers(CodebaseContext context) {
|
|
||||||
Object cached = context.getCache().get(TRIGGERS_CACHE);
|
|
||||||
if (cached instanceof List<?> list) {
|
|
||||||
return (List<TriggerPoint>) list;
|
|
||||||
}
|
|
||||||
List<TriggerPoint> triggers = new ArrayList<>();
|
|
||||||
GenericEventDetector detector =
|
|
||||||
new GenericEventDetector(context, context.getConstantResolver(), context.getLibraryHints());
|
|
||||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
|
||||||
triggers.addAll(detector.detect(cu));
|
|
||||||
}
|
|
||||||
context.getCache().put(TRIGGERS_CACHE, triggers);
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CallGraphEngine createCallGraphEngine(CodebaseContext 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);
|
|
||||||
return new JdtCallGraphEngine(context, injectionAnalyzer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String entryPointKey(EntryPoint entryPoint) {
|
|
||||||
if (entryPoint.getName() != null && !entryPoint.getName().isBlank()) {
|
|
||||||
return entryPoint.getName();
|
|
||||||
}
|
|
||||||
return entryPoint.getClassName() + "#" + entryPoint.getMethodName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts normalized domain keys from state machine configuration class names
|
|
||||||
* (e.g. {@code StandardOrderStateMachineConfiguration} → {@code ORDER}).
|
|
||||||
*/
|
|
||||||
public final class MachineDomainKeys {
|
|
||||||
|
|
||||||
private MachineDomainKeys() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String extractMachineDomainKey(String machineName) {
|
|
||||||
String simple = machineName.substring(machineName.lastIndexOf('.') + 1);
|
|
||||||
String upper = simple.toUpperCase(Locale.ROOT);
|
|
||||||
for (String marker : List.of("STATEMACHINECONFIGURATION", "STATEMACHINE", "CONFIGURATION", "CONFIG")) {
|
|
||||||
int idx = upper.indexOf(marker);
|
|
||||||
if (idx > 0) {
|
|
||||||
return normalizeDomainKey(upper.substring(0, idx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int stateIdx = upper.indexOf("STATE");
|
|
||||||
if (stateIdx > 0) {
|
|
||||||
return normalizeDomainKey(upper.substring(0, stateIdx));
|
|
||||||
}
|
|
||||||
return normalizeDomainKey(upper);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeDomainKey(String domain) {
|
|
||||||
if (domain == null || domain.isEmpty()) {
|
|
||||||
return domain;
|
|
||||||
}
|
|
||||||
String upper = domain.toUpperCase(Locale.ROOT);
|
|
||||||
if (upper.startsWith("STANDARD") && upper.length() > "STANDARD".length()) {
|
|
||||||
return upper.substring("STANDARD".length());
|
|
||||||
}
|
|
||||||
return upper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
|
||||||
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.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters codebase-wide analysis artifacts to those relevant for a single state machine config.
|
|
||||||
*/
|
|
||||||
public final class MachineScopeFilter {
|
|
||||||
|
|
||||||
private static final BeanResolutionEngine ROUTING = new HeuristicBeanResolutionEngine();
|
|
||||||
|
|
||||||
private MachineScopeFilter() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<TriggerPoint> filterTriggersForMachine(
|
|
||||||
List<TriggerPoint> triggers, String machineName, CodebaseContext context) {
|
|
||||||
return filterTriggersForMachine(triggers, machineName, context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<TriggerPoint> filterTriggersForMachine(
|
|
||||||
List<TriggerPoint> triggers,
|
|
||||||
String machineName,
|
|
||||||
CodebaseContext context,
|
|
||||||
List<Transition> machineTransitions) {
|
|
||||||
if (triggers == null || machineName == null) {
|
|
||||||
return triggers == null ? List.of() : triggers;
|
|
||||||
}
|
|
||||||
List<TriggerPoint> filtered = new ArrayList<>();
|
|
||||||
for (TriggerPoint trigger : triggers) {
|
|
||||||
if (isTriggerForMachine(trigger, machineName, context, machineTransitions)) {
|
|
||||||
filtered.add(trigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CallChain> filterCallChainsForMachine(
|
|
||||||
List<CallChain> chains, String machineName, CodebaseContext context) {
|
|
||||||
return filterCallChainsForMachine(chains, machineName, context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CallChain> filterCallChainsForMachine(
|
|
||||||
List<CallChain> chains,
|
|
||||||
String machineName,
|
|
||||||
CodebaseContext context,
|
|
||||||
List<Transition> machineTransitions) {
|
|
||||||
if (chains == null || machineName == null) {
|
|
||||||
return chains == null ? List.of() : chains;
|
|
||||||
}
|
|
||||||
List<CallChain> filtered = new ArrayList<>();
|
|
||||||
String machineEventTypeFqn = resolveMachineEventTypeFqn(machineName, context, machineTransitions);
|
|
||||||
for (CallChain chain : chains) {
|
|
||||||
if (ROUTING.hasProvenMachineAffinity(chain, machineName, context, machineTransitions)) {
|
|
||||||
filtered.add(chain);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint();
|
|
||||||
if (trigger != null
|
|
||||||
&& machineEventTypeFqn != null
|
|
||||||
&& CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context)) {
|
|
||||||
filtered.add(chain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String resolveMachineEventTypeFqn(
|
|
||||||
String machineName,
|
|
||||||
CodebaseContext context,
|
|
||||||
List<Transition> machineTransitions) {
|
|
||||||
if (context != null && machineName != null) {
|
|
||||||
String[] types = StateMachineTypeResolver.resolve(machineName, context);
|
|
||||||
if (types != null && types.length > 1 && types[1] != null && !types[1].isBlank()) {
|
|
||||||
return types[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (machineTransitions == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (Transition transition : machineTransitions) {
|
|
||||||
Event event = transition.getEvent();
|
|
||||||
if (event == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String fullIdentifier = event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
|
|
||||||
if (fullIdentifier != null && fullIdentifier.contains(".")) {
|
|
||||||
return fullIdentifier.substring(0, fullIdentifier.lastIndexOf('.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<EntryPoint> filterEntryPointsForMachine(
|
|
||||||
List<EntryPoint> entryPoints, String machineName, CodebaseContext context) {
|
|
||||||
if (entryPoints == null || machineName == null) {
|
|
||||||
return entryPoints == null ? List.of() : entryPoints;
|
|
||||||
}
|
|
||||||
List<EntryPoint> filtered = new ArrayList<>();
|
|
||||||
for (EntryPoint entryPoint : entryPoints) {
|
|
||||||
if (isEntryPointForMachine(entryPoint, machineName, context)) {
|
|
||||||
filtered.add(entryPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEntryPointForMachine(
|
|
||||||
EntryPoint entryPoint, String machineName, CodebaseContext context) {
|
|
||||||
if (entryPoint == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (EntryPointScopeResolver.hasPathVariablePlaceholder(entryPoint)) {
|
|
||||||
EntryPointScopeResolver.Affinity affinity =
|
|
||||||
EntryPointScopeResolver.resolveAffinity(entryPoint, machineName, context);
|
|
||||||
return affinity != EntryPointScopeResolver.Affinity.AGAINST_MACHINE;
|
|
||||||
}
|
|
||||||
|
|
||||||
EntryPointScopeResolver.Affinity affinity =
|
|
||||||
EntryPointScopeResolver.resolveAffinity(entryPoint, machineName, context);
|
|
||||||
if (affinity == EntryPointScopeResolver.Affinity.FOR_MACHINE) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (affinity == EntryPointScopeResolver.Affinity.AGAINST_MACHINE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isTriggerForMachine(
|
|
||||||
TriggerPoint trigger, String machineName, CodebaseContext context, List<Transition> machineTransitions) {
|
|
||||||
if (trigger == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
CallChain probe = CallChain.builder().triggerPoint(trigger).methodChain(List.of()).build();
|
|
||||||
return ROUTING.hasProvenMachineAffinity(probe, machineName, context, machineTransitions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,35 +3,26 @@ package click.kamil.springstatemachineexporter.analysis.enricher;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
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 java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
import click.kamil.springstatemachineexporter.analysis.enricher.matching.HeuristicEventMatchingEngine;
|
||||||
|
|
||||||
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||||
|
|
||||||
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
|
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
|
||||||
|
private final EventMatchingEngine matchingEngine = new HeuristicEventMatchingEngine();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
@@ -39,25 +30,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine(context);
|
|
||||||
|
|
||||||
List<CallChain> updatedChains = new ArrayList<>();
|
List<CallChain> updatedChains = new ArrayList<>();
|
||||||
List<Transition> stateMachineTransitions = result.getTransitions();
|
List<Transition> stateMachineTransitions = result.getTransitions();
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
|
||||||
JsonExportContextFactory.resolveMachineTypes(
|
|
||||||
result.getName(),
|
|
||||||
result.getStateTypeFqn(),
|
|
||||||
result.getEventTypeFqn(),
|
|
||||||
context);
|
|
||||||
StateMachineTypeResolver.MachineTypes effectiveMachineTypes =
|
|
||||||
CallChainLinkPolicy.resolveEffectiveMachineTypes(
|
|
||||||
result.getName(),
|
|
||||||
result.getStateTypeFqn(),
|
|
||||||
result.getEventTypeFqn(),
|
|
||||||
machineTypes,
|
|
||||||
stateMachineTransitions,
|
|
||||||
context);
|
|
||||||
String effectiveEventTypeFqn = effectiveMachineTypes.eventTypeFqn();
|
|
||||||
|
|
||||||
for (CallChain chain : result.getMetadata().getCallChains()) {
|
for (CallChain chain : result.getMetadata().getCallChains()) {
|
||||||
TriggerPoint tp = chain.getTriggerPoint();
|
TriggerPoint tp = chain.getTriggerPoint();
|
||||||
@@ -66,129 +40,37 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
tp = MachineEnumCanonicalizer.expandBoundValueOfFromConstraints(
|
String triggerEvent = simplify(tp.getEvent());
|
||||||
tp, effectiveMachineTypes, context, chain.getEntryPoint());
|
String triggerSource = tp.getSourceState() != null ? simplify(tp.getSourceState()) : null;
|
||||||
tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
|
||||||
tp, effectiveMachineTypes, context, stateMachineTransitions, true);
|
|
||||||
tp = CallChainPolyNarrower.narrowForCallChain(
|
|
||||||
tp, chain, effectiveMachineTypes, result.getEventTypeFqn(), stateMachineTransitions, context);
|
|
||||||
tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, effectiveMachineTypes, context);
|
|
||||||
tp = CallChainLinkPolicy.applyRestExternalPolicy(tp, chain, context);
|
|
||||||
tp = tp.toBuilder()
|
|
||||||
.eventTypeFqn(tp.getEventTypeFqn() != null
|
|
||||||
? tp.getEventTypeFqn()
|
|
||||||
: effectiveEventTypeFqn)
|
|
||||||
.stateTypeFqn(tp.getStateTypeFqn() != null
|
|
||||||
? tp.getStateTypeFqn()
|
|
||||||
: effectiveMachineTypes.stateTypeFqn())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) {
|
|
||||||
updatedChains.add(chain);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tp.isExternal() && isUnresolvedGenericRestEndpoint(chain.getEntryPoint())) {
|
|
||||||
updatedChains.add(chain.toBuilder()
|
|
||||||
.triggerPoint(tp)
|
|
||||||
.matchedTransitions(null)
|
|
||||||
.linkResolution(LinkResolution.UNRESOLVED_EXTERNAL)
|
|
||||||
.build());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String triggerSource = tp.getSourceState() != null ? simplifySourceState(tp.getSourceState()) : null;
|
|
||||||
if (CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
|
|
||||||
tp,
|
|
||||||
effectiveEventTypeFqn,
|
|
||||||
context)) {
|
|
||||||
updatedChains.add(chain.toBuilder()
|
|
||||||
.triggerPoint(tp)
|
|
||||||
.matchedTransitions(null)
|
|
||||||
.linkResolution(LinkResolution.AMBIGUOUS_WIDEN)
|
|
||||||
.build());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean ambiguousSource = false;
|
|
||||||
|
|
||||||
if (triggerSource == null) {
|
|
||||||
Map<String, Set<String>> sourcesByEvent = new LinkedHashMap<>();
|
|
||||||
Map<String, Set<String>> stateTypesByEvent = new LinkedHashMap<>();
|
|
||||||
Map<String, String> canonicalSourceBySimplified = new LinkedHashMap<>();
|
|
||||||
for (Transition t : stateMachineTransitions) {
|
|
||||||
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)
|
|
||||||
&& isRoutedToCorrectMachine(
|
|
||||||
chain, result.getName(), context, stateMachineTransitions, tp,
|
|
||||||
effectiveEventTypeFqn)
|
|
||||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
|
||||||
String smEventForLink = canonicalEvent(t.getEvent());
|
|
||||||
for (State smSourceState : t.getSourceStates()) {
|
|
||||||
String smSourceForLink = canonicalState(smSourceState);
|
|
||||||
String smSource = simplifySourceState(smSourceForLink);
|
|
||||||
sourcesByEvent.computeIfAbsent(smEventForLink, ignored -> new LinkedHashSet<>())
|
|
||||||
.add(smSource);
|
|
||||||
stateTypesByEvent.computeIfAbsent(smEventForLink, ignored -> new LinkedHashSet<>())
|
|
||||||
.add(extractStateEnumType(smSourceForLink));
|
|
||||||
canonicalSourceBySimplified.putIfAbsent(smSource, smSourceForLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
boolean crossStateTypeConflict = stateTypesByEvent.values().stream()
|
|
||||||
.anyMatch(types -> types.size() > 1);
|
|
||||||
if (crossStateTypeConflict) {
|
|
||||||
ambiguousSource = true;
|
|
||||||
} else if (sourcesByEvent.values().stream().anyMatch(sources -> sources.size() > 1)) {
|
|
||||||
ambiguousSource = !CallChainLinkPolicy.isEndpointNarrowPolyEvidence(
|
|
||||||
tp,
|
|
||||||
effectiveEventTypeFqn,
|
|
||||||
context);
|
|
||||||
} else {
|
|
||||||
Set<String> allDistinctSources = new LinkedHashSet<>();
|
|
||||||
sourcesByEvent.values().forEach(allDistinctSources::addAll);
|
|
||||||
if (allDistinctSources.size() == 1) {
|
|
||||||
String inferredSimplifiedSource = allDistinctSources.iterator().next();
|
|
||||||
String inferredCanonicalSource = canonicalSourceBySimplified.get(inferredSimplifiedSource);
|
|
||||||
tp = tp.toBuilder().sourceState(inferredCanonicalSource).build();
|
|
||||||
triggerSource = inferredSimplifiedSource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<MatchedTransition> matched = new ArrayList<>();
|
List<MatchedTransition> matched = new ArrayList<>();
|
||||||
|
|
||||||
if (!ambiguousSource) {
|
|
||||||
for (Transition t : stateMachineTransitions) {
|
for (Transition t : stateMachineTransitions) {
|
||||||
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
|
||||||
String smEventForLink = canonicalEvent(t.getEvent());
|
String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
|
||||||
|
// Event matches
|
||||||
for (State smSourceState : t.getSourceStates()) {
|
for (State smSourceState : t.getSourceStates()) {
|
||||||
String smSourceForLink = canonicalState(smSourceState);
|
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
|
||||||
String smSource = simplifySourceState(smSourceForLink);
|
String smSource = simplify(smSourceRaw);
|
||||||
if (triggerSource == null || triggerSource.equals(smSource)) {
|
if (triggerSource == null || triggerSource.equals(smSource)) {
|
||||||
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
|
||||||
MatchedTransition mt = MatchedTransition.builder()
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
.sourceState(smSourceForLink)
|
.sourceState(smSourceRaw)
|
||||||
.targetState(smSourceForLink)
|
.targetState(smSourceRaw)
|
||||||
.event(smEventForLink)
|
.event(smEventRaw)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(
|
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||||
chain, result.getName(), context, stateMachineTransitions, tp,
|
|
||||||
effectiveEventTypeFqn)
|
|
||||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
|
||||||
matched.add(mt);
|
matched.add(mt);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (State smTargetState : t.getTargetStates()) {
|
for (State smTargetState : t.getTargetStates()) {
|
||||||
String targetForLink = canonicalState(smTargetState);
|
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
|
||||||
MatchedTransition mt = MatchedTransition.builder()
|
MatchedTransition mt = MatchedTransition.builder()
|
||||||
.sourceState(smSourceForLink)
|
.sourceState(smSourceRaw)
|
||||||
.targetState(targetForLink)
|
.targetState(targetRaw)
|
||||||
.event(smEventForLink)
|
.event(smEventRaw)
|
||||||
.build();
|
.build();
|
||||||
if (isRoutedToCorrectMachine(
|
if (isRoutedToCorrectMachine(chain, result.getName())) {
|
||||||
chain, result.getName(), context, stateMachineTransitions, tp,
|
|
||||||
effectiveEventTypeFqn)
|
|
||||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
|
||||||
matched.add(mt);
|
matched.add(mt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,46 +79,20 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (ambiguousSource) {
|
|
||||||
tp = tp.toBuilder().ambiguous(true).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
LinkResolution linkResolution = CallChainLinkPolicy.resolveLinkResolution(
|
|
||||||
tp,
|
|
||||||
matched,
|
|
||||||
ambiguousSource,
|
|
||||||
effectiveEventTypeFqn,
|
|
||||||
context);
|
|
||||||
|
|
||||||
if (!matched.isEmpty()) {
|
if (!matched.isEmpty()) {
|
||||||
CallChain newChain = chain.toBuilder()
|
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
|
||||||
.triggerPoint(tp)
|
|
||||||
.matchedTransitions(matched)
|
|
||||||
.linkResolution(linkResolution)
|
|
||||||
.build();
|
|
||||||
updatedChains.add(newChain);
|
updatedChains.add(newChain);
|
||||||
} else {
|
} else {
|
||||||
updatedChains.add(chain.toBuilder()
|
updatedChains.add(chain);
|
||||||
.triggerPoint(tp)
|
|
||||||
.matchedTransitions(null)
|
|
||||||
.linkResolution(linkResolution)
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
|
// Update the metadata with the new call chains
|
||||||
result.getMetadata().getTriggers(), result.getName(), context, stateMachineTransitions);
|
|
||||||
List<CallChain> scopedChains = MachineScopeFilter.filterCallChainsForMachine(
|
|
||||||
updatedChains, result.getName(), context, stateMachineTransitions);
|
|
||||||
List<EntryPoint> scopedEntryPoints = EntryPointScopeResolver.scopeFromCallChains(
|
|
||||||
scopedChains, result.getMetadata().getEntryPoints());
|
|
||||||
|
|
||||||
CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
|
CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
|
||||||
.triggers(scopedTriggers)
|
.triggers(result.getMetadata().getTriggers())
|
||||||
.entryPoints(scopedEntryPoints)
|
.entryPoints(result.getMetadata().getEntryPoints())
|
||||||
.callChains(scopedChains)
|
.callChains(updatedChains)
|
||||||
.properties(result.getMetadata().getProperties())
|
.properties(result.getMetadata().getProperties())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -244,106 +100,45 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean isRoutedToCorrectMachine(
|
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||||
CallChain chain,
|
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName);
|
||||||
String currentMachineName,
|
|
||||||
CodebaseContext context,
|
|
||||||
List<Transition> machineTransitions,
|
|
||||||
TriggerPoint triggerPoint,
|
|
||||||
String machineEventTypeFqn) {
|
|
||||||
if (triggerPoint != null
|
|
||||||
&& machineEventTypeFqn != null
|
|
||||||
&& CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(triggerPoint, machineEventTypeFqn, context)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return routingEngine.hasProvenMachineAffinity(chain, currentMachineName, context, machineTransitions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRoutedToCorrectMachine(
|
private String simplify(String name) {
|
||||||
CallChain chain, String currentMachineName, CodebaseContext context, List<Transition> machineTransitions) {
|
|
||||||
return isRoutedToCorrectMachine(
|
|
||||||
chain, currentMachineName, context, machineTransitions, chain.getTriggerPoint(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConstraintCompatible(String constraint, String machineName) {
|
|
||||||
if (constraint == null || machineName == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String machineConstraint = stripEventBindingClauses(constraint);
|
|
||||||
return BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
|
|
||||||
machineConstraint, MachineDomainKeys.extractMachineDomainKey(machineName));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripEventBindingClauses(String constraint) {
|
|
||||||
if (constraint == null || constraint.isBlank()) {
|
|
||||||
return constraint;
|
|
||||||
}
|
|
||||||
String stripped = constraint.replaceAll("\"[^\"]+\"\\.equalsIgnoreCase\\(event\\)", "true");
|
|
||||||
stripped = stripped.replaceAll("&&\\s*&&+", "&&");
|
|
||||||
stripped = stripped.replaceAll("^\\s*&&\\s*", "");
|
|
||||||
stripped = stripped.replaceAll("\\s*&&\\s*$", "");
|
|
||||||
return stripped.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractStateEnumType(String stateIdentifier) {
|
|
||||||
if (stateIdentifier == null || stateIdentifier.isBlank()) {
|
|
||||||
return stateIdentifier;
|
|
||||||
}
|
|
||||||
int lastDot = stateIdentifier.lastIndexOf('.');
|
|
||||||
if (lastDot <= 0) {
|
|
||||||
return stateIdentifier;
|
|
||||||
}
|
|
||||||
return stateIdentifier.substring(0, lastDot);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final java.util.Map<String, String> simplifySourceCache = new java.util.HashMap<>();
|
|
||||||
|
|
||||||
private String simplifySourceState(String name) {
|
|
||||||
if (name == null) return null;
|
if (name == null) return null;
|
||||||
if (simplifySourceCache.containsKey(name)) return simplifySourceCache.get(name);
|
// Strip common suffixes
|
||||||
|
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||||
// For source states we must preserve enum type context to avoid collisions between
|
if (simplified.isEmpty()) {
|
||||||
// different state enums that share a constant name (e.g. OrderState.NEW vs InvoiceState.NEW).
|
simplified = name;
|
||||||
// Use EnumFormat.fn-like suffix (Type.CONST) when possible.
|
|
||||||
String simplified = name;
|
|
||||||
int lastDot = name.lastIndexOf('.');
|
|
||||||
if (lastDot > 0) {
|
|
||||||
int prevDot = name.lastIndexOf('.', lastDot - 1);
|
|
||||||
if (prevDot > 0) {
|
|
||||||
simplified = name.substring(prevDot + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Simplify full identifiers to just the last part (enum name)
|
||||||
simplifySourceCache.put(name, simplified);
|
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;
|
return simplified;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String canonicalState(State state) {
|
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||||
if (state == null) {
|
// Only fire if it looks like a simple identifier (no spaces, no parens)
|
||||||
return null;
|
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
|
||||||
}
|
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
|
||||||
return state.fullIdentifier() != null ? state.fullIdentifier() : state.rawName();
|
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
|
||||||
}
|
|
||||||
|
|
||||||
private String canonicalEvent(click.kamil.springstatemachineexporter.model.Event event) {
|
|
||||||
if (event == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isUnresolvedGenericRestEndpoint(EntryPoint entryPoint) {
|
|
||||||
if (entryPoint == null) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (entryPoint.getName() != null && entryPoint.getName().contains("{")) {
|
|
||||||
return true;
|
String shorter = smEvent.length() < triggerEvent.length() ? smEvent : triggerEvent;
|
||||||
}
|
String longer = smEvent.length() >= triggerEvent.length() ? smEvent : triggerEvent;
|
||||||
if (entryPoint.getMetadata() != null) {
|
|
||||||
Object path = entryPoint.getMetadata().get("path");
|
shorter = shorter.toLowerCase();
|
||||||
if (path instanceof String pathValue && pathValue.contains("{")) {
|
longer = longer.toLowerCase();
|
||||||
return true;
|
|
||||||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
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.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Canonicalizes trigger-side enum identifiers ({@code event}, {@code polymorphicEvents},
|
|
||||||
* {@code sourceState}) using the owning machine config's {@code <State, Event>} types.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
|
||||||
if (result.getMetadata() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes = JsonExportContextFactory.resolveMachineTypes(
|
|
||||||
result.getName(), result.getStateTypeFqn(), result.getEventTypeFqn(), context);
|
|
||||||
if (machineTypes.stateTypeFqn() == null && machineTypes.eventTypeFqn() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TriggerPoint> triggers = result.getMetadata().getTriggers();
|
|
||||||
List<TriggerPoint> canonicalTriggers = triggers == null ? null : triggers.stream()
|
|
||||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeAndExpandTriggerPoint(
|
|
||||||
trigger, machineTypes, context))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
List<CallChain> callChains = result.getMetadata().getCallChains();
|
|
||||||
List<CallChain> canonicalChains = callChains == null ? null : callChains.stream()
|
|
||||||
.map(chain -> {
|
|
||||||
if (chain.getTriggerPoint() == null) {
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
TriggerPoint expanded = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
|
||||||
chain.getTriggerPoint(), machineTypes, context, result.getTransitions(), true);
|
|
||||||
TriggerPoint canonical = expanded.getEventTypeFqn() == null && expanded.getStateTypeFqn() == null
|
|
||||||
? MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(
|
|
||||||
expanded, machineTypes, context)
|
|
||||||
: MachineEnumCanonicalizer.canonicalizeTriggerPoint(
|
|
||||||
expanded, machineTypes, context);
|
|
||||||
return chain.toBuilder().triggerPoint(canonical).build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (canonicalTriggers == null && canonicalChains == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Canonicalized triggers for {}", result.getName());
|
|
||||||
|
|
||||||
result.setMetadata(CodebaseMetadata.builder()
|
|
||||||
.triggers(canonicalTriggers != null ? canonicalTriggers : triggers)
|
|
||||||
.entryPoints(result.getMetadata().getEntryPoints())
|
|
||||||
.callChains(canonicalChains != null ? canonicalChains : callChains)
|
|
||||||
.properties(result.getMetadata().getProperties())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,11 @@ package click.kamil.springstatemachineexporter.analysis.enricher;
|
|||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TriggerEnricher implements AnalysisEnricher {
|
public class TriggerEnricher implements AnalysisEnricher {
|
||||||
@@ -17,15 +14,11 @@ public class TriggerEnricher implements AnalysisEnricher {
|
|||||||
@Override
|
@Override
|
||||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||||
log.info("Enriching {} with triggers", result.getName());
|
log.info("Enriching {} with triggers", result.getName());
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
List<TriggerPoint> triggers = intelligence.findTriggerPoints();
|
||||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
|
||||||
|
// Initially, we just add all triggers found in the codebase to the metadata.
|
||||||
List<TriggerPoint> triggers = intelligence.findTriggerPoints().stream()
|
// In later steps of Phase 2, we will filter them by State Machine ID.
|
||||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
triggers = MachineScopeFilter.filterTriggersForMachine(triggers, result.getName(), context);
|
|
||||||
|
|
||||||
result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||||
.triggers(triggers)
|
.triggers(triggers)
|
||||||
.build());
|
.build());
|
||||||
|
|||||||
@@ -6,54 +6,22 @@ import java.util.List;
|
|||||||
|
|
||||||
public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
||||||
|
|
||||||
private final java.util.Map<java.util.Map.Entry<Event, TriggerPoint>, Boolean> matchesCache = new java.util.HashMap<>();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) {
|
public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) {
|
||||||
if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) {
|
if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
java.util.Map.Entry<Event, TriggerPoint> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(stateMachineEvent, triggerPoint);
|
|
||||||
if (matchesCache.containsKey(cacheKey)) return matchesCache.get(cacheKey);
|
|
||||||
|
|
||||||
boolean result = calculateMatches(stateMachineEvent, triggerPoint);
|
String triggerEvent = simplify(triggerPoint.getEvent());
|
||||||
matchesCache.put(cacheKey, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean calculateMatches(Event stateMachineEvent, TriggerPoint triggerPoint) {
|
|
||||||
|
|
||||||
String rawTriggerEvent = triggerPoint.getEvent();
|
|
||||||
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
||||||
String smEvent = simplify(smEventRaw);
|
String smEvent = simplify(smEventRaw);
|
||||||
|
|
||||||
boolean isWildcard = isWildcardVariable(rawTriggerEvent);
|
boolean isWildcard = isWildcardVariable(triggerEvent);
|
||||||
|
|
||||||
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
||||||
|
|
||||||
boolean hasPolyMatch = false;
|
boolean hasPolyMatch = false;
|
||||||
for (String pe : polyEvents) {
|
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;
|
String simplePe = pe;
|
||||||
if (pe.contains(".")) {
|
if (pe.contains(".")) {
|
||||||
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
|
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
|
||||||
@@ -61,132 +29,28 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
|||||||
String simplifiedPe = simplify(simplePe);
|
String simplifiedPe = simplify(simplePe);
|
||||||
|
|
||||||
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
||||||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent) ||
|
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) {
|
||||||
isGuardedContains(smEvent, simplifiedPe)) {
|
|
||||||
hasPolyMatch = true;
|
hasPolyMatch = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasPolyMatch) return true;
|
return hasPolyMatch || smEvent.equals(triggerEvent) || (polyEvents.isEmpty() && isWildcard);
|
||||||
if (polyEvents.isEmpty() && isWildcard) {
|
|
||||||
String eventTypeFqn = triggerPoint.getEventTypeFqn();
|
|
||||||
if (eventTypeFqn != null && smEventRaw.contains(".")) {
|
|
||||||
String smEventClass = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
|
||||||
if (smEventClass.equals(eventTypeFqn)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String simpleEventType = eventTypeFqn.contains(".") ? eventTypeFqn.substring(eventTypeFqn.lastIndexOf('.') + 1) : eventTypeFqn;
|
|
||||||
String simpleSmClass = smEventClass.contains(".") ? smEventClass.substring(smEventClass.lastIndexOf('.') + 1) : smEventClass;
|
|
||||||
if (simpleEventType.equalsIgnoreCase(simpleSmClass)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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 final java.util.Map<String, String> simplifyCache = new java.util.HashMap<>();
|
|
||||||
private final java.util.Map<java.util.Map.Entry<String, String>, Boolean> guardedCache = new java.util.HashMap<>();
|
|
||||||
private static final java.util.regex.Pattern SUFFIX_PATTERN = java.util.regex.Pattern.compile("(?i)(.+)(Event|Action|Transition|Command)(s)?$");
|
|
||||||
private static final java.util.regex.Pattern FQN_PATTERN = java.util.regex.Pattern.compile("^.*\\.([A-Z0-9_]+)$");
|
|
||||||
|
|
||||||
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
|
||||||
if (smEvent == null || triggerEvent == null) return false;
|
|
||||||
|
|
||||||
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(smEvent, triggerEvent);
|
|
||||||
if (guardedCache.containsKey(cacheKey)) return guardedCache.get(cacheKey);
|
|
||||||
|
|
||||||
boolean result = calculateGuardedContains(smEvent, triggerEvent);
|
|
||||||
guardedCache.put(cacheKey, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean calculateGuardedContains(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 smLower = smEvent.toLowerCase();
|
|
||||||
String triggerLower = triggerEvent.toLowerCase();
|
|
||||||
|
|
||||||
if (smLower.equals(triggerLower)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match if one contains the other as a whole word/segment.
|
|
||||||
// We quote the patterns to avoid regex syntax injection issues.
|
|
||||||
String pattern1 = "\\b" + java.util.regex.Pattern.quote(smLower) + "\\b";
|
|
||||||
String pattern2 = "\\b" + java.util.regex.Pattern.quote(triggerLower) + "\\b";
|
|
||||||
|
|
||||||
// Treat underscores and hyphens as word boundary separators
|
|
||||||
String smNormalized = smLower.replace('_', ' ').replace('-', ' ');
|
|
||||||
String triggerNormalized = triggerLower.replace('_', ' ').replace('-', ' ');
|
|
||||||
|
|
||||||
return smNormalized.matches(".*" + pattern2 + ".*") || triggerNormalized.matches(".*" + pattern1 + ".*");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWildcardVariable(String eventStr) {
|
private boolean isWildcardVariable(String eventStr) {
|
||||||
if (eventStr == null) return false;
|
return eventStr.equals("event") || eventStr.equals("e") ||
|
||||||
|
eventStr.equals("msg") || eventStr.equals("message") ||
|
||||||
if (eventStr.equals("event") || eventStr.equals("e") ||
|
eventStr.equals("payload");
|
||||||
eventStr.equals("msg") || eventStr.equals("message") ||
|
|
||||||
eventStr.equals("payload")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventStr.contains(".valueOf(") || eventStr.contains("valueOf (")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventStr.contains(" ? ") && eventStr.contains(" : ")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String simplify(String name) {
|
private String simplify(String name) {
|
||||||
if (name == null) return null;
|
if (name == null) return null;
|
||||||
if (simplifyCache.containsKey(name)) return simplifyCache.get(name);
|
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||||
|
|
||||||
String simplified = SUFFIX_PATTERN.matcher(name).replaceAll("$1");
|
|
||||||
if (simplified.isEmpty()) {
|
if (simplified.isEmpty()) {
|
||||||
simplified = name;
|
simplified = name;
|
||||||
}
|
}
|
||||||
simplified = FQN_PATTERN.matcher(simplified).replaceAll("$1");
|
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
|
||||||
|
|
||||||
simplifyCache.put(name, simplified);
|
|
||||||
return simplified;
|
return simplified;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
|
||||||
|
|
||||||
private final CodebaseContext context;
|
|
||||||
|
|
||||||
public StrictFqnMatchingEngine() {
|
|
||||||
this(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StrictFqnMatchingEngine(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) {
|
|
||||||
if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers
|
|
||||||
.isLifecycle(triggerPoint.getEvent())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
|
||||||
String rawTriggerEvent = triggerPoint.getEvent();
|
|
||||||
|
|
||||||
if (triggerPoint.isExternal()) {
|
|
||||||
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null
|
|
||||||
? triggerPoint.getPolymorphicEvents()
|
|
||||||
: java.util.Collections.emptyList();
|
|
||||||
if (polyEvents.isEmpty() && isDynamicVariable(rawTriggerEvent)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawTriggerEvent != null && rawTriggerEvent.startsWith("<SYMBOLIC: ") && rawTriggerEvent.endsWith(".*>")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
|
|
||||||
|
|
||||||
for (String pe : polyEvents) {
|
|
||||||
if (pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String eventTypeFqn = triggerPoint.getEventTypeFqn();
|
|
||||||
if (pe.contains(".") && isStringTypeOrPrimitive(eventTypeFqn)) {
|
|
||||||
eventTypeFqn = null;
|
|
||||||
}
|
|
||||||
if (matchEventNames(pe, smEventRaw, eventTypeFqn)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!pe.contains(".") && smEventRaw.contains(".")) {
|
|
||||||
String smConst = constantName(smEventRaw);
|
|
||||||
if (pe.equals(smConst) && triggerPoint.getEventTypeFqn() != null
|
|
||||||
&& !isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
|
|
||||||
String smEnumType = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
|
|
||||||
return typesMatch(triggerPoint.getEventTypeFqn(), smEnumType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawTriggerEvent != null && rawTriggerEvent.contains(".valueOf(")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polyEvents.isEmpty() && isDynamicVariable(rawTriggerEvent)) {
|
|
||||||
if (triggerPoint.getEventTypeFqn() != null && !isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Getter/method-call expressions (e.g. richEvent.getId(), getType()) without resolved
|
|
||||||
// polymorphic events must not wildcard-match bare or enum transition events.
|
|
||||||
if (rawTriggerEvent.contains(".") || rawTriggerEvent.endsWith("()")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (triggerPoint.getEventTypeFqn() != null && isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
|
|
||||||
return !smEventRaw.contains(".");
|
|
||||||
}
|
|
||||||
return !smEventRaw.contains(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchEventNames(rawTriggerEvent, smEventRaw, triggerPoint.getEventTypeFqn());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean matchEventNames(String triggerEvent, String smEvent, String eventTypeFqn) {
|
|
||||||
if (triggerEvent == null || smEvent == null) return false;
|
|
||||||
if (triggerEvent.equals(smEvent)) return true;
|
|
||||||
|
|
||||||
String tConst = constantName(triggerEvent);
|
|
||||||
String smConst = constantName(smEvent);
|
|
||||||
|
|
||||||
if (!tConst.equals(smConst)) return false;
|
|
||||||
|
|
||||||
// String-typed triggers may match configured string transition events by constant name.
|
|
||||||
if (isStringTypeOrPrimitive(eventTypeFqn) && smEvent.contains(".") && !triggerEvent.contains(".")) {
|
|
||||||
String smEnumType = smEvent.substring(0, smEvent.lastIndexOf('.'));
|
|
||||||
return isStringTypeOrPrimitive(smEnumType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent matching enum triggers to string transitions
|
|
||||||
if (!smEvent.contains(".") && triggerEvent.contains(".")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (smEvent.contains(".")) {
|
|
||||||
String smEnumType = smEvent.substring(0, smEvent.lastIndexOf('.'));
|
|
||||||
|
|
||||||
if (eventTypeFqn != null) {
|
|
||||||
return typesMatch(eventTypeFqn, smEnumType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (triggerEvent.contains(".")) {
|
|
||||||
String triggerEnumType = triggerEvent.substring(0, triggerEvent.lastIndexOf('.'));
|
|
||||||
return typesMatch(triggerEnumType, smEnumType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bare constant name (e.g. after Enum.name()) with no type context must not match every enum
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both sides are unqualified identifiers (string/primitive events)
|
|
||||||
return !triggerEvent.contains(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean typesMatch(String type1, String type2) {
|
|
||||||
if (MachineEnumCanonicalizer.enumTypesMatch(type1, type2, context)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (type1 != null && type2 != null) {
|
|
||||||
String machineFqn = type1.contains(".") ? type1 : type2;
|
|
||||||
String candidate = type1.contains(".") ? type2 : type1;
|
|
||||||
return MachineEnumCanonicalizer.enumTypesMatchForMachine(machineFqn, candidate, context);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String constantName(String event) {
|
|
||||||
return event.contains(".") ? event.substring(event.lastIndexOf('.') + 1) : event;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isStringTypeOrPrimitive(String typeFqn) {
|
|
||||||
if (typeFqn == null) return false;
|
|
||||||
return typeFqn.equals("String") || typeFqn.equals("java.lang.String") ||
|
|
||||||
typeFqn.equals("int") || typeFqn.equals("long") || typeFqn.equals("char");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDynamicVariable(String eventStr) {
|
|
||||||
if (eventStr == null || eventStr.isEmpty()) return false;
|
|
||||||
|
|
||||||
if (eventStr.contains(" ? ") && eventStr.contains(" : ")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventStr.endsWith("()")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventStr.contains(".")) {
|
|
||||||
String constantPart = eventStr.substring(eventStr.lastIndexOf('.') + 1);
|
|
||||||
if (!constantPart.isEmpty() && Character.isUpperCase(constantPart.charAt(0))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String firstPart = eventStr.substring(0, eventStr.indexOf('.'));
|
|
||||||
if (!firstPart.isEmpty() && Character.isLowerCase(firstPart.charAt(0))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Character.isLowerCase(eventStr.charAt(0))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
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();
|
|
||||||
|
|
||||||
if (caller != null) {
|
|
||||||
String callerVal = resolver.resolve(caller, context);
|
|
||||||
if (callerVal != null && !callerVal.isEmpty()) {
|
|
||||||
addValue(collectedConstants, callerVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String argVal = resolver.resolve(arg, context);
|
|
||||||
if (argVal != null && !argVal.isEmpty()) {
|
|
||||||
addValue(collectedConstants, argVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean visit(InfixExpression node) {
|
|
||||||
if (node.getOperator() == InfixExpression.Operator.EQUALS) {
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
|
||||||
String left = resolver.resolve(node.getLeftOperand(), context);
|
|
||||||
if (left != null && !left.isEmpty()) {
|
|
||||||
addValue(collectedConstants, left);
|
|
||||||
}
|
|
||||||
String right = resolver.resolve(node.getRightOperand(), context);
|
|
||||||
if (right != null && !right.isEmpty()) {
|
|
||||||
addValue(collectedConstants, right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean visit(SwitchStatement node) {
|
|
||||||
for (Object stmt : node.statements()) {
|
|
||||||
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
|
|
||||||
for (Object expr : sc.expressions()) {
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
|
||||||
String val = resolver.resolve((Expression) expr, context);
|
|
||||||
if (val != null && !val.isEmpty()) {
|
|
||||||
addValue(collectedConstants, val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean visit(SwitchExpression node) {
|
|
||||||
for (Object stmt : node.statements()) {
|
|
||||||
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
|
|
||||||
for (Object expr : sc.expressions()) {
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
|
||||||
String val = resolver.resolve((Expression) expr, context);
|
|
||||||
if (val != null && !val.isEmpty()) {
|
|
||||||
addValue(collectedConstants, val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectedConstants.isEmpty()) return null;
|
|
||||||
return new ArrayList<>(collectedConstants);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addValue(Set<String> collectedConstants, String val) {
|
|
||||||
if (val.startsWith("ENUM_SET:")) {
|
|
||||||
for (String eVal : val.substring(9).split(",")) {
|
|
||||||
String parsed = parseEnumSetElement(eVal);
|
|
||||||
if (!collectedConstants.contains(parsed)) collectedConstants.add(parsed);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
collectedConstants.add(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseEnumSetElement(String element) {
|
|
||||||
if (element.contains(".")) {
|
|
||||||
return element.substring(element.lastIndexOf('.') + 1);
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface BeanResolutionEngine {
|
public interface BeanResolutionEngine {
|
||||||
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context);
|
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName);
|
||||||
|
|
||||||
default boolean hasProvenMachineAffinity(
|
|
||||||
CallChain chain,
|
|
||||||
String currentMachineName,
|
|
||||||
CodebaseContext context,
|
|
||||||
List<Transition> machineTransitions) {
|
|
||||||
return isRoutedToCorrectMachine(chain, currentMachineName, context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.Annotation;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps {@code @EnableStateMachine(name=…)} bean names to configuration class FQNs.
|
|
||||||
*/
|
|
||||||
final class EnableStateMachineBeanRouting {
|
|
||||||
|
|
||||||
private EnableStateMachineBeanRouting() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static Boolean matchesNamedBean(String beanName, String currentMachineConfigFqn, CodebaseContext context) {
|
|
||||||
String normalized = stripQuotes(beanName);
|
|
||||||
if (normalized == null || normalized.isEmpty() || context == null || currentMachineConfigFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String matchingConfig = findConfigFqnByBeanName(normalized, context);
|
|
||||||
if (matchingConfig == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return currentMachineConfigFqn.equals(matchingConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String findConfigFqnByBeanName(String beanName, CodebaseContext context) {
|
|
||||||
for (TypeDeclaration configType : context.findEntryPointClasses(java.util.List.of("EnableStateMachine"))) {
|
|
||||||
String configFqn = context.getFqn(configType);
|
|
||||||
String configuredName = extractEnableStateMachineName(configType);
|
|
||||||
if (configuredName == null || configuredName.isEmpty()) {
|
|
||||||
configuredName = defaultBeanName(configType.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
if (beanName.equals(configuredName)) {
|
|
||||||
return configFqn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String extractEnableStateMachineName(TypeDeclaration configType) {
|
|
||||||
for (Object modifierObj : configType.modifiers()) {
|
|
||||||
if (!(modifierObj instanceof Annotation annotation)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!annotation.getTypeName().toString().endsWith("EnableStateMachine")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String name = AstUtils.extractAnnotationMember(annotation, "name");
|
|
||||||
if (name != null && !name.isBlank()) {
|
|
||||||
return stripQuotes(name);
|
|
||||||
}
|
|
||||||
String value = AstUtils.extractAnnotationMember(annotation, "value");
|
|
||||||
if (value != null && !value.isBlank()) {
|
|
||||||
return stripQuotes(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String defaultBeanNameFromFqn(String fqn) {
|
|
||||||
if (fqn == null || !fqn.contains(".")) {
|
|
||||||
return fqn;
|
|
||||||
}
|
|
||||||
String simple = fqn.substring(fqn.lastIndexOf('.') + 1);
|
|
||||||
return defaultBeanName(simple);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String defaultBeanName(String simpleClassName) {
|
|
||||||
if (simpleClassName == null || simpleClassName.isEmpty()) {
|
|
||||||
return simpleClassName;
|
|
||||||
}
|
|
||||||
return Character.toLowerCase(simpleClassName.charAt(0)) + simpleClassName.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripQuotes(String raw) {
|
|
||||||
if (raw == null || raw.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String trimmed = raw.trim();
|
|
||||||
if (trimmed.length() >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
|
||||||
return trimmed.substring(1, trimmed.length() - 1);
|
|
||||||
}
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,341 +1,188 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||||
|
|
||||||
private enum DistinctTypeAffinity {
|
|
||||||
FOR_MACHINE,
|
|
||||||
AGAINST_MACHINE,
|
|
||||||
UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasProvenMachineAffinity(
|
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
|
||||||
CallChain chain,
|
String targetVar = chain.getContextMachineId();
|
||||||
String currentMachineName,
|
if (targetVar == null && chain.getTriggerPoint() != null) {
|
||||||
CodebaseContext context,
|
targetVar = chain.getTriggerPoint().getStateMachineId();
|
||||||
List<Transition> machineTransitions) {
|
|
||||||
if (chain == null) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (currentMachineName == null) {
|
|
||||||
return isRoutedToCorrectMachine(chain, currentMachineName, context);
|
String simplifiedMachineName = currentMachineName != null ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() : "";
|
||||||
}
|
|
||||||
|
if (targetVar != null && !targetVar.isEmpty()) {
|
||||||
MachineRoutingEvidence evidence = MachineRoutingEvidence.from(chain);
|
targetVar = targetVar.toLowerCase();
|
||||||
|
if (targetVar.endsWith("statemachine")) {
|
||||||
String explicitTarget = evidence.explicitBeanName();
|
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
|
||||||
if (explicitTarget != null && !explicitTarget.isEmpty() && context != null) {
|
if (!prefix.isEmpty()) {
|
||||||
Boolean namedTarget = EnableStateMachineBeanRouting.matchesNamedBean(
|
if (simplifiedMachineName.contains(prefix)) {
|
||||||
explicitTarget, currentMachineName, context);
|
return true; // Explicit positive match
|
||||||
if (namedTarget != null) {
|
} else {
|
||||||
return namedTarget;
|
return false; // Explicit negative match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (simplifiedMachineName.contains(targetVar)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chain.getTriggerPoint() != null && context != null) {
|
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) {
|
||||||
DistinctTypeAffinity typeAffinity =
|
String smPackage = getPackageName(currentMachineName);
|
||||||
resolveDistinctTypeAffinity(chain.getTriggerPoint(), currentMachineName, context);
|
String smSimple = getSimpleClassName(currentMachineName);
|
||||||
if (typeAffinity == DistinctTypeAffinity.FOR_MACHINE) {
|
String smPrefix = getFirstCamelCaseWord(smSimple);
|
||||||
|
|
||||||
|
boolean hasPositiveMatch = false;
|
||||||
|
boolean hasStrongMismatch = false;
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
hasStrongMismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPositiveMatch) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (typeAffinity == DistinctTypeAffinity.AGAINST_MACHINE) {
|
if (hasStrongMismatch) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (context == null || isSingleStateMachine(context)) {
|
private boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
||||||
return isRoutedToCorrectMachine(chain, currentMachineName, context);
|
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
||||||
}
|
|
||||||
|
if (smPackage.startsWith(chainPackage) || chainPackage.startsWith(smPackage)) {
|
||||||
TriggerPoint trigger = chain.getTriggerPoint();
|
|
||||||
if (trigger != null
|
|
||||||
&& SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)
|
|
||||||
&& matchesConfiguredTransitionEvent(trigger, machineTransitions, context)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isRoutedToCorrectMachine(chain, currentMachineName, context);
|
// 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) {
|
||||||
|
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
|
||||||
|
|
||||||
@Override
|
String[] p1 = smPackage.split("\\.");
|
||||||
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
String[] p2 = chainPackage.split("\\.");
|
||||||
MachineRoutingEvidence evidence = MachineRoutingEvidence.from(chain);
|
|
||||||
|
int matchIdx = -1;
|
||||||
String explicitTarget = evidence.explicitBeanName();
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
if (explicitTarget != null && !explicitTarget.isEmpty() && context != null && currentMachineName != null) {
|
if (p1[i].equals(p2[i])) {
|
||||||
Boolean namedTarget = EnableStateMachineBeanRouting.matchesNamedBean(
|
matchIdx = i;
|
||||||
explicitTarget, currentMachineName, context);
|
} else {
|
||||||
if (namedTarget != null) {
|
break;
|
||||||
return namedTarget;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasExplicitBeanTarget = explicitTarget != null && !explicitTarget.isEmpty();
|
Set<String> smDivergentTerms = new HashSet<>();
|
||||||
|
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase());
|
||||||
// Precise FQN Type argument match from JDT bindings at sendEvent site
|
for (int i = matchIdx + 1; i < p1.length; i++) {
|
||||||
if (!hasExplicitBeanTarget && chain.getTriggerPoint() != null && context != null && currentMachineName != null) {
|
smDivergentTerms.add(p1[i].toLowerCase());
|
||||||
String triggerEventFqn = chain.getTriggerPoint().getEventTypeFqn();
|
|
||||||
String triggerStateFqn = chain.getTriggerPoint().getStateTypeFqn();
|
|
||||||
if (triggerEventFqn != null || triggerStateFqn != null) {
|
|
||||||
String[] machineTypes = StateMachineTypeResolver.resolve(currentMachineName, context);
|
|
||||||
String machineStateFqn = machineTypes[0];
|
|
||||||
String machineEventFqn = machineTypes[1];
|
|
||||||
boolean matched = false;
|
|
||||||
boolean mismatched = false;
|
|
||||||
|
|
||||||
if (triggerEventFqn != null && machineEventFqn != null) {
|
|
||||||
if (typesEquivalent(triggerEventFqn, machineEventFqn)) {
|
|
||||||
matched = true;
|
|
||||||
} else if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
|
||||||
mismatched = true;
|
|
||||||
} else if (isSingleStateMachine(context)
|
|
||||||
&& (isErasedOrOpaqueType(triggerEventFqn) || isErasedOrOpaqueType(machineEventFqn))) {
|
|
||||||
matched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (triggerStateFqn != null && machineStateFqn != null) {
|
|
||||||
if (typesEquivalent(triggerStateFqn, machineStateFqn)) {
|
|
||||||
matched = true;
|
|
||||||
} else if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
|
||||||
mismatched = true;
|
|
||||||
} else if (isSingleStateMachine(context)
|
|
||||||
&& (isErasedOrOpaqueType(triggerStateFqn) || isErasedOrOpaqueType(machineStateFqn))) {
|
|
||||||
matched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matched) {
|
|
||||||
if (!isAmbiguousSharedGenericMatch(
|
|
||||||
triggerEventFqn, triggerStateFqn, machineStateFqn, machineEventFqn)
|
|
||||||
|| isSingleStateMachine(context)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mismatched) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Fail closed only when both trigger and machine generic types were resolved but
|
|
||||||
// could not be reconciled. When machine config AST does not expose type args (stub
|
|
||||||
// config classes in tests, generated configs), fall through to single-SM / package routing.
|
|
||||||
boolean machineTypesKnown = machineEventFqn != null || machineStateFqn != null;
|
|
||||||
if (machineTypesKnown) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSingleStateMachine(context) && isAmbiguousSharedGenericTrigger(chain.getTriggerPoint())) {
|
Set<String> chainDivergentTerms = new HashSet<>();
|
||||||
Boolean injectionMatch = SpringInjectionRouting.matchesMachineConfig(
|
if (chainPrefix != null) chainDivergentTerms.add(chainPrefix.toLowerCase());
|
||||||
chain.getTriggerPoint(), currentMachineName, context);
|
for (int i = matchIdx + 1; i < p2.length; i++) {
|
||||||
if (injectionMatch != null) {
|
chainDivergentTerms.add(p2[i].toLowerCase());
|
||||||
return injectionMatch;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Boolean packageMatch = PackageNameRoutingHeuristics.matches(chain, currentMachineName, explicitTarget);
|
if (!smDivergentTerms.isEmpty() && !chainDivergentTerms.isEmpty()) {
|
||||||
if (packageMatch != null) {
|
Set<String> intersection = new HashSet<>(smDivergentTerms);
|
||||||
return packageMatch;
|
intersection.retainAll(chainDivergentTerms);
|
||||||
}
|
return intersection.isEmpty();
|
||||||
return context == null || isSingleStateMachine(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DistinctTypeAffinity resolveDistinctTypeAffinity(
|
|
||||||
TriggerPoint trigger, String machineName, CodebaseContext context) {
|
|
||||||
String triggerEventFqn = trigger.getEventTypeFqn();
|
|
||||||
String triggerStateFqn = trigger.getStateTypeFqn();
|
|
||||||
if (triggerEventFqn == null && triggerStateFqn == null) {
|
|
||||||
return DistinctTypeAffinity.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] machineTypes = StateMachineTypeResolver.resolve(machineName, context);
|
|
||||||
String machineStateFqn = machineTypes[0];
|
|
||||||
String machineEventFqn = machineTypes[1];
|
|
||||||
|
|
||||||
boolean matched = false;
|
|
||||||
boolean mismatched = false;
|
|
||||||
|
|
||||||
if (triggerEventFqn != null && machineEventFqn != null) {
|
|
||||||
if (typesEquivalent(triggerEventFqn, machineEventFqn)
|
|
||||||
&& !isAmbiguousSharedGenericType(triggerEventFqn)) {
|
|
||||||
matched = true;
|
|
||||||
} else if (isDistinctTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
|
||||||
mismatched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (triggerStateFqn != null && machineStateFqn != null) {
|
|
||||||
if (typesEquivalent(triggerStateFqn, machineStateFqn)
|
|
||||||
&& !isAmbiguousSharedGenericType(triggerStateFqn)) {
|
|
||||||
matched = true;
|
|
||||||
} else if (isDistinctTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
|
||||||
mismatched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matched) {
|
|
||||||
return DistinctTypeAffinity.FOR_MACHINE;
|
|
||||||
}
|
|
||||||
if (mismatched) {
|
|
||||||
return DistinctTypeAffinity.AGAINST_MACHINE;
|
|
||||||
}
|
|
||||||
return DistinctTypeAffinity.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean matchesConfiguredTransitionEvent(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
List<Transition> machineTransitions,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
EventMatchingEngine matchingEngine = new StrictFqnMatchingEngine(context);
|
|
||||||
for (Transition transition : machineTransitions) {
|
|
||||||
if (transition.getEvent() != null && matchingEngine.matches(transition.getEvent(), trigger)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAmbiguousSharedGenericMatch(
|
private Set<String> extractDomainTerms(String pkg, String prefix) {
|
||||||
String triggerEventFqn,
|
Set<String> terms = new HashSet<>();
|
||||||
String triggerStateFqn,
|
if (pkg != null && !pkg.isEmpty()) {
|
||||||
String machineStateFqn,
|
String[] segments = pkg.split("\\.");
|
||||||
String machineEventFqn) {
|
// Take segments from index 2 onwards if length > 2
|
||||||
boolean eventShared = triggerEventFqn != null
|
int start = segments.length > 2 ? 2 : 0;
|
||||||
&& machineEventFqn != null
|
for (int i = start; i < segments.length; i++) {
|
||||||
&& typesEquivalent(triggerEventFqn, machineEventFqn)
|
terms.add(segments[i].toLowerCase());
|
||||||
&& isAmbiguousSharedGenericType(triggerEventFqn);
|
|
||||||
boolean stateShared = triggerStateFqn != null
|
|
||||||
&& machineStateFqn != null
|
|
||||||
&& typesEquivalent(triggerStateFqn, machineStateFqn)
|
|
||||||
&& isAmbiguousSharedGenericType(triggerStateFqn);
|
|
||||||
return eventShared || stateShared;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAmbiguousSharedGenericType(String typeFqn) {
|
|
||||||
return isErasedOrOpaqueType(typeFqn) || isStringType(typeFqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDistinctTypeMismatched(String type1, String type2) {
|
|
||||||
if (isAmbiguousSharedGenericType(type1) || isAmbiguousSharedGenericType(type2)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isTypeMismatched(type1, type2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isStringType(String typeFqn) {
|
|
||||||
if (typeFqn == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String erased = eraseGenerics(typeFqn);
|
|
||||||
return "java.lang.String".equals(erased) || "String".equals(erased);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean resolveNamedBeanTarget(String beanName, String currentMachineName, CodebaseContext context) {
|
|
||||||
return EnableStateMachineBeanRouting.matchesNamedBean(beanName, currentMachineName, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAmbiguousSharedGenericTrigger(TriggerPoint trigger) {
|
|
||||||
if (trigger == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isAmbiguousSharedGenericType(trigger.getEventTypeFqn())
|
|
||||||
|| isAmbiguousSharedGenericType(trigger.getStateTypeFqn());
|
|
||||||
}
|
|
||||||
|
|
||||||
private int countStateMachines(CodebaseContext context) {
|
|
||||||
if (context == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Set<String> machines = new HashSet<>();
|
|
||||||
for (TypeDeclaration td : context.findEntryPointClasses(
|
|
||||||
java.util.List.of("EnableStateMachineFactory", "EnableStateMachine"))) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (fqn != null) {
|
|
||||||
machines.add(fqn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (TypeDeclaration td : context.getTypeDeclarations()) {
|
if (prefix != null && !prefix.isEmpty()) {
|
||||||
if (td == null || td.isInterface()) {
|
terms.add(prefix.toLowerCase());
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (context.extendsStateMachineConfigurerAdapter(td)) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (fqn != null) {
|
|
||||||
machines.add(fqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!machines.isEmpty()) {
|
return terms;
|
||||||
return machines.size();
|
|
||||||
}
|
|
||||||
return context.findBeanMethodsReturning(
|
|
||||||
java.util.Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory")).size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String eraseGenerics(String type) {
|
private String getPackageName(String fqn) {
|
||||||
if (type == null) return null;
|
if (fqn == null || !fqn.contains(".")) return "";
|
||||||
int idx = type.indexOf('<');
|
return fqn.substring(0, fqn.lastIndexOf('.'));
|
||||||
if (idx != -1) {
|
}
|
||||||
return type.substring(0, idx);
|
|
||||||
|
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('('));
|
||||||
}
|
}
|
||||||
return type;
|
if (clean.contains(".")) {
|
||||||
}
|
clean = clean.substring(0, clean.lastIndexOf('.'));
|
||||||
|
|
||||||
private boolean typesEquivalent(String type1, String type2) {
|
|
||||||
if (type1 == null || type2 == null) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
String erased1 = normalizePrimitiveFqn(eraseGenerics(type1));
|
return clean;
|
||||||
String erased2 = normalizePrimitiveFqn(eraseGenerics(type2));
|
|
||||||
return erased1.equals(erased2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizePrimitiveFqn(String type) {
|
private String getFirstCamelCaseWord(String simpleName) {
|
||||||
if (type == null) {
|
if (simpleName == null || simpleName.isEmpty()) return null;
|
||||||
return null;
|
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
|
||||||
|
if (words.length > 0) {
|
||||||
|
return words[0];
|
||||||
}
|
}
|
||||||
return switch (type) {
|
return null;
|
||||||
case "java.lang.String" -> "String";
|
|
||||||
case "java.lang.Object" -> "Object";
|
|
||||||
case "java.io.Serializable" -> "Serializable";
|
|
||||||
default -> type;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTypeMismatched(String type1, String type2) {
|
|
||||||
if (type1 == null || type2 == null) return false;
|
|
||||||
type1 = eraseGenerics(type1);
|
|
||||||
type2 = eraseGenerics(type2);
|
|
||||||
if (type1.length() == 1 || type2.length() == 1) return false;
|
|
||||||
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 isErasedOrOpaqueType(String typeFqn) {
|
|
||||||
if (typeFqn == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String erased = eraseGenerics(typeFqn);
|
|
||||||
return "java.lang.Object".equals(erased) || "Object".equals(erased)
|
|
||||||
|| "java.io.Serializable".equals(erased) || "Serializable".equals(erased);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSingleStateMachine(CodebaseContext context) {
|
|
||||||
return countStateMachines(context) <= 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Source-derived evidence used to decide whether a call chain belongs to a state-machine export.
|
|
||||||
*/
|
|
||||||
public record MachineRoutingEvidence(
|
|
||||||
String explicitBeanName,
|
|
||||||
String stateMachineId,
|
|
||||||
String eventTypeFqn,
|
|
||||||
String stateTypeFqn,
|
|
||||||
String triggerClassFqn,
|
|
||||||
String triggerMethod,
|
|
||||||
Integer triggerLine) {
|
|
||||||
|
|
||||||
public static MachineRoutingEvidence from(CallChain chain) {
|
|
||||||
if (chain == null) {
|
|
||||||
return new MachineRoutingEvidence(null, null, null, null, null, null, null);
|
|
||||||
}
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint();
|
|
||||||
String explicit = chain.getContextMachineId();
|
|
||||||
String stateMachineId = null;
|
|
||||||
Integer line = null;
|
|
||||||
if (trigger != null) {
|
|
||||||
if (explicit == null) {
|
|
||||||
explicit = trigger.getStateMachineId();
|
|
||||||
}
|
|
||||||
stateMachineId = trigger.getStateMachineId();
|
|
||||||
if (trigger.getLineNumber() > 0) {
|
|
||||||
line = trigger.getLineNumber();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new MachineRoutingEvidence(
|
|
||||||
explicit,
|
|
||||||
stateMachineId,
|
|
||||||
trigger != null ? trigger.getEventTypeFqn() : null,
|
|
||||||
trigger != null ? trigger.getStateTypeFqn() : null,
|
|
||||||
trigger != null ? trigger.getClassName() : null,
|
|
||||||
trigger != null ? trigger.getMethodName() : null,
|
|
||||||
line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy package-prefix and domain-name matching for single-machine / null-context fallback routing.
|
|
||||||
*/
|
|
||||||
final class PackageNameRoutingHeuristics {
|
|
||||||
|
|
||||||
private static final Set<String> GENERIC_CLASS_PREFIXES = Set.of(
|
|
||||||
"state", "statemachine", "config", "configuration", "adapter",
|
|
||||||
"enterprise", "app", "global", "core", "project", "main",
|
|
||||||
"base", "common", "shared");
|
|
||||||
|
|
||||||
private static final Set<String> GENERIC_PACKAGE_SEGMENTS = 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", "shared", "common",
|
|
||||||
"global", "core", "base", "main", "app", "enterprise");
|
|
||||||
|
|
||||||
private PackageNameRoutingHeuristics() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true when any class in the call chain carries a vertical domain prefix or package segment
|
|
||||||
*/
|
|
||||||
static boolean hasVerticalDomainOwnership(CallChain chain) {
|
|
||||||
if (chain == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (chain.getTriggerPoint() != null && chain.getTriggerPoint().getClassName() != null) {
|
|
||||||
if (classHasVerticalDomainOwnership(chain.getTriggerPoint().getClassName())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (chain.getMethodChain() != null) {
|
|
||||||
for (String method : chain.getMethodChain()) {
|
|
||||||
if (classHasVerticalDomainOwnership(getClassNameOnly(method))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true/false if package heuristics decide, null if inconclusive
|
|
||||||
*/
|
|
||||||
static Boolean matches(CallChain chain, String currentMachineName, String explicitBeanTarget) {
|
|
||||||
if (chain == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean hasExplicitBeanTarget = explicitBeanTarget != null && !explicitBeanTarget.isEmpty();
|
|
||||||
String targetVar = hasExplicitBeanTarget ? explicitBeanTarget : 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;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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 (hasMatchingDomainPrefix(smPrefix, 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 (hasMatchingDomainPrefix(smPrefix, 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 null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
|
|
||||||
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (smPackage.equals(chainPackage)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chainPackage.startsWith(smPackage + ".") && !smPackage.startsWith(chainPackage + ".")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (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 static 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();
|
|
||||||
|
|
||||||
if (isGenericClassPrefix(smLower) || isGenericClassPrefix(chainLower)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!smLower.equals(chainLower)) {
|
|
||||||
if (chainPackage.toLowerCase().contains(smLower) || smPackage.toLowerCase().contains(chainLower)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
boolean onlyGenericDivergence = true;
|
|
||||||
for (int i = matchIdx + 1; i < p1.length; i++) {
|
|
||||||
if (!isGenericPackageSegment(p1[i])) {
|
|
||||||
onlyGenericDivergence = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (onlyGenericDivergence) {
|
|
||||||
for (int i = matchIdx + 1; i < p2.length; i++) {
|
|
||||||
if (!isGenericPackageSegment(p2[i])) {
|
|
||||||
onlyGenericDivergence = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!onlyGenericDivergence) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getPackageName(String fqn) {
|
|
||||||
if (fqn == null || !fqn.contains(".")) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return fqn.substring(0, fqn.lastIndexOf('.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getSimpleClassName(String fqn) {
|
|
||||||
if (fqn == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (!fqn.contains(".")) {
|
|
||||||
return fqn;
|
|
||||||
}
|
|
||||||
return fqn.substring(fqn.lastIndexOf('.') + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static 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 static 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 static boolean classHasVerticalDomainOwnership(String classFqn) {
|
|
||||||
if (classFqn == null || classFqn.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String prefix = getFirstCamelCaseWord(getSimpleClassName(classFqn));
|
|
||||||
if (prefix != null && !isGenericClassPrefix(prefix)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return hasVerticalPackageSegment(getPackageName(classFqn));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasVerticalPackageSegment(String packageName) {
|
|
||||||
if (packageName == null || packageName.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String[] segments = packageName.split("\\.");
|
|
||||||
for (int i = 2; i < segments.length; i++) {
|
|
||||||
if (!isGenericPackageSegment(segments[i])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasMatchingDomainPrefix(String smPrefix, String chainPrefix) {
|
|
||||||
return smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isGenericClassPrefix(String prefix) {
|
|
||||||
return prefix != null && GENERIC_CLASS_PREFIXES.contains(prefix.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isGenericPackageSegment(String segment) {
|
|
||||||
return segment != null && GENERIC_PACKAGE_SEGMENTS.contains(segment.toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decides whether a call chain is provably shared infrastructure that may intentionally
|
|
||||||
* attach to multiple state machines when they share the same transition event.
|
|
||||||
* <p>
|
|
||||||
* Shared infrastructure requires source-derived evidence only: no distinct machine enum types
|
|
||||||
* at the sendEvent site and no vertical domain ownership in the call chain classes/packages.
|
|
||||||
* Event-name matching alone is never sufficient outside this policy.
|
|
||||||
*/
|
|
||||||
final class SharedServiceRoutingPolicy {
|
|
||||||
|
|
||||||
private SharedServiceRoutingPolicy() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true when the chain may multi-attach to every machine that shares its transition event
|
|
||||||
*/
|
|
||||||
static boolean isProvablySharedInfrastructure(CallChain chain) {
|
|
||||||
if (chain == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
MachineRoutingEvidence evidence = MachineRoutingEvidence.from(chain);
|
|
||||||
if (evidence.eventTypeFqn() != null || evidence.stateTypeFqn() != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !PackageNameRoutingHeuristics.hasVerticalDomainOwnership(chain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
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 org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves which {@code @EnableStateMachine} configuration owns a trigger's {@code StateMachine} receiver
|
|
||||||
* using Spring injection analysis (JDT bindings), not package-name guessing.
|
|
||||||
*/
|
|
||||||
final class SpringInjectionRouting {
|
|
||||||
|
|
||||||
private static final Set<String> TRIGGER_METHODS = Set.of(
|
|
||||||
"sendEvent", "sendEvents", "sendEventCollect", "sendEventMono", "fire", "trigger");
|
|
||||||
|
|
||||||
private SpringInjectionRouting() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static Boolean matchesMachineConfig(TriggerPoint trigger, String machineConfigFqn, CodebaseContext context) {
|
|
||||||
if (trigger == null || machineConfigFqn == null || context == null || !context.isResolveBindings()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String configFqn = resolveConfigurationFqn(trigger, context);
|
|
||||||
if (configFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return machineConfigFqn.equals(configFqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveConfigurationFqn(TriggerPoint trigger, CodebaseContext context) {
|
|
||||||
if (trigger == null || trigger.getClassName() == null || trigger.getMethodName() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!context.isResolveBindings()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
InjectionPointAnalyzer analyzer = createAnalyzer(context);
|
|
||||||
TypeDeclaration type = context.getTypeDeclaration(trigger.getClassName());
|
|
||||||
if (type == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodDeclaration method = context.findMethodDeclaration(type, trigger.getMethodName(), true);
|
|
||||||
if (method == null || method.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
CompilationUnit cu = (CompilationUnit) type.getRoot();
|
|
||||||
final IVariableBinding[] receiverBinding = new IVariableBinding[1];
|
|
||||||
method.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (trigger.getLineNumber() > 0 && cu != null
|
|
||||||
&& cu.getLineNumber(node.getStartPosition()) != trigger.getLineNumber()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!TRIGGER_METHODS.contains(node.getName().getIdentifier())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Expression receiver = node.getExpression();
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
IBinding binding = sn.resolveBinding();
|
|
||||||
if (binding instanceof IVariableBinding vb) {
|
|
||||||
receiverBinding[0] = vb;
|
|
||||||
}
|
|
||||||
} else if (receiver instanceof FieldAccess fa) {
|
|
||||||
receiverBinding[0] = fa.resolveFieldBinding();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (receiverBinding[0] == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String injectedFqn = analyzer.resolveInjectedBeanFqn(receiverBinding[0]);
|
|
||||||
if (injectedFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration injectedType = context.getTypeDeclaration(injectedFqn);
|
|
||||||
if (injectedType != null && context.extendsStateMachineConfigurerAdapter(injectedType)) {
|
|
||||||
return injectedFqn;
|
|
||||||
}
|
|
||||||
return EnableStateMachineBeanRouting.findConfigFqnByBeanName(
|
|
||||||
EnableStateMachineBeanRouting.defaultBeanNameFromFqn(injectedFqn), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InjectionPointAnalyzer createAnalyzer(CodebaseContext context) {
|
|
||||||
SpringBeanRegistry registry = new SpringBeanRegistry();
|
|
||||||
SpringContextScanner scanner = new SpringContextScanner(registry);
|
|
||||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
|
||||||
cu.accept(scanner);
|
|
||||||
}
|
|
||||||
return new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves constant values for fields referenced by indexed trivial accessors.
|
|
||||||
*/
|
|
||||||
public final class AccessorFieldResolver {
|
|
||||||
|
|
||||||
private AccessorFieldResolver() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> resolveFieldConstants(
|
|
||||||
AccessorSummary accessor,
|
|
||||||
String requestingTypeFqn,
|
|
||||||
CodebaseContext context,
|
|
||||||
ConstructorAnalyzer constructorAnalyzer,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
Set<String> visited,
|
|
||||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> constantExtractor) {
|
|
||||||
if (accessor == null || !accessor.isGetter() || context == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
if (accessor.kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER
|
|
||||||
|| accessor.fieldName() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
String traceTypeFqn = requestingTypeFqn != null && !requestingTypeFqn.isBlank()
|
|
||||||
? requestingTypeFqn
|
|
||||||
: accessor.declaringFqn();
|
|
||||||
TypeDeclaration typeDeclaration = contextCu != null
|
|
||||||
? context.getTypeDeclaration(traceTypeFqn, contextCu)
|
|
||||||
: null;
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
typeDeclaration = context.getTypeDeclaration(traceTypeFqn);
|
|
||||||
}
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
if (constructorAnalyzer != null) {
|
|
||||||
constants.addAll(constructorAnalyzer.traceFieldInConstructors(
|
|
||||||
typeDeclaration, accessor.fieldName(), context, visited));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constants.isEmpty()) {
|
|
||||||
collectFieldInitializerConstants(
|
|
||||||
typeDeclaration, accessor.fieldName(), constantExtractor, constants, context);
|
|
||||||
}
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFieldInitializerConstants(
|
|
||||||
TypeDeclaration typeDeclaration,
|
|
||||||
String fieldName,
|
|
||||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> constantExtractor,
|
|
||||||
List<String> constants,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (constantExtractor == null || typeDeclaration == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
|
||||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(fieldName)
|
|
||||||
&& fragment.getInitializer() != null) {
|
|
||||||
constantExtractor.accept(fragment.getInitializer(), constants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (constants.isEmpty() && context != null) {
|
|
||||||
String superFqn = context.getSuperclassFqn(typeDeclaration);
|
|
||||||
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
|
|
||||||
TypeDeclaration superType = context.getTypeDeclaration(superFqn);
|
|
||||||
if (superType != null) {
|
|
||||||
collectFieldInitializerConstants(superType, fieldName, constantExtractor, constants, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class AccessorIndex {
|
|
||||||
|
|
||||||
private static final AccessorIndex EMPTY = new AccessorIndex(Map.of(), 0, 0, 0);
|
|
||||||
|
|
||||||
private final Map<String, Map<String, AccessorSummary>> byOwnerAndMethod;
|
|
||||||
private final int typeCount;
|
|
||||||
private final int trivialCount;
|
|
||||||
private final int blockedCount;
|
|
||||||
|
|
||||||
public AccessorIndex(
|
|
||||||
Map<String, Map<String, AccessorSummary>> byOwnerAndMethod,
|
|
||||||
int typeCount,
|
|
||||||
int trivialCount,
|
|
||||||
int blockedCount) {
|
|
||||||
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
|
|
||||||
this.typeCount = typeCount;
|
|
||||||
this.trivialCount = trivialCount;
|
|
||||||
this.blockedCount = blockedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AccessorIndex empty() {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> lookup(String ownerFqn, String methodName) {
|
|
||||||
if (ownerFqn == null || methodName == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Map<String, AccessorSummary> methods = byOwnerAndMethod.get(ownerFqn);
|
|
||||||
if (methods == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.ofNullable(methods.get(methodName));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<AccessorSummary> accessorsForType(String ownerFqn) {
|
|
||||||
Map<String, AccessorSummary> methods = byOwnerAndMethod.get(ownerFqn);
|
|
||||||
if (methods == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return List.copyOf(methods.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Map<String, AccessorSummary>> asMap() {
|
|
||||||
return byOwnerAndMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int typeCount() {
|
|
||||||
return typeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int trivialCount() {
|
|
||||||
return trivialCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int blockedCount() {
|
|
||||||
return blockedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Map<String, AccessorSummary>> deepCopy(Map<String, Map<String, AccessorSummary>> source) {
|
|
||||||
Map<String, Map<String, AccessorSummary>> copy = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Map<String, AccessorSummary>> entry : source.entrySet()) {
|
|
||||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class AccessorIndexBuilder {
|
|
||||||
|
|
||||||
private final TrivialAccessorDetector detector = new TrivialAccessorDetector();
|
|
||||||
private CodebaseContext context;
|
|
||||||
|
|
||||||
public AccessorIndex build(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
Map<String, DirectTypeAccessors> direct = new HashMap<>();
|
|
||||||
|
|
||||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
|
||||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
|
||||||
for (Object typeObj : cu.types()) {
|
|
||||||
if (typeObj instanceof TypeDeclaration td) {
|
|
||||||
indexTypeDeclaration(td, direct);
|
|
||||||
} else if (typeObj instanceof RecordDeclaration rd) {
|
|
||||||
indexRecord(rd, packageName, direct);
|
|
||||||
} else if (typeObj instanceof EnumDeclaration ed) {
|
|
||||||
indexEnum(ed, packageName, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int blocked = direct.values().stream().mapToInt(state -> state.blockedMethods.size()).sum();
|
|
||||||
|
|
||||||
Map<String, Map<String, AccessorSummary>> effective = new HashMap<>();
|
|
||||||
int trivial = 0;
|
|
||||||
for (String ownerFqn : direct.keySet()) {
|
|
||||||
Map<String, AccessorSummary> merged = mergeEffective(ownerFqn, direct, effective, new HashSet<>());
|
|
||||||
if (!merged.isEmpty()) {
|
|
||||||
effective.put(ownerFqn, merged);
|
|
||||||
trivial += merged.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Built accessor index: {} trivial accessors across {} types ({} blocked overrides)",
|
|
||||||
trivial,
|
|
||||||
effective.size(),
|
|
||||||
blocked);
|
|
||||||
|
|
||||||
this.context = null;
|
|
||||||
return new AccessorIndex(effective, effective.size(), trivial, blocked);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexTypeDeclaration(TypeDeclaration td, Map<String, DirectTypeAccessors> direct) {
|
|
||||||
String ownerFqn = context.getFqn(td);
|
|
||||||
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
|
|
||||||
for (MethodDeclaration md : td.getMethods()) {
|
|
||||||
classifyMethod(ownerFqn, md, fieldName -> findFieldDeclaringFqn(td, fieldName), state);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (TypeDeclaration nested : td.getTypes()) {
|
|
||||||
indexTypeDeclaration(nested, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexEnum(EnumDeclaration ed, String parentFqn, Map<String, DirectTypeAccessors> direct) {
|
|
||||||
String ownerFqn = parentFqn.isEmpty() ? ed.getName().getIdentifier() : parentFqn + "." + ed.getName().getIdentifier();
|
|
||||||
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
|
|
||||||
|
|
||||||
for (Object decl : ed.bodyDeclarations()) {
|
|
||||||
if (decl instanceof MethodDeclaration md) {
|
|
||||||
classifyMethod(ownerFqn, md, fieldName -> findEnumFieldDeclaringFqn(ed, ownerFqn, fieldName), state);
|
|
||||||
} else if (decl instanceof TypeDeclaration nestedTd) {
|
|
||||||
indexTypeDeclaration(nestedTd, direct);
|
|
||||||
} else if (decl instanceof RecordDeclaration nestedRd) {
|
|
||||||
indexRecord(nestedRd, ownerFqn, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexRecord(RecordDeclaration rd, String parentFqn, Map<String, DirectTypeAccessors> direct) {
|
|
||||||
String ownerFqn = parentFqn.isEmpty() ? rd.getName().getIdentifier() : parentFqn + "." + rd.getName().getIdentifier();
|
|
||||||
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
|
|
||||||
|
|
||||||
for (Object compObj : rd.recordComponents()) {
|
|
||||||
String componentName = recordComponentName(compObj);
|
|
||||||
if (componentName == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
detector.detectRecordComponent(ownerFqn, componentName).ifPresent(summary ->
|
|
||||||
state.trivial.put(summary.methodName(), summary));
|
|
||||||
}
|
|
||||||
|
|
||||||
TrivialAccessorDetector.FieldLookup recordFieldLookup =
|
|
||||||
fieldName -> findRecordFieldDeclaringFqn(rd, ownerFqn, fieldName);
|
|
||||||
|
|
||||||
for (Object declObj : rd.bodyDeclarations()) {
|
|
||||||
if (declObj instanceof MethodDeclaration md) {
|
|
||||||
String methodName = md.getName().getIdentifier();
|
|
||||||
AccessorSummary existing = state.trivial.get(methodName);
|
|
||||||
if (existing != null && existing.kind() == AccessorKind.RECORD_COMPONENT) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
classifyMethod(ownerFqn, md, recordFieldLookup, state);
|
|
||||||
} else if (declObj instanceof TypeDeclaration nestedTd) {
|
|
||||||
indexTypeDeclaration(nestedTd, direct);
|
|
||||||
} else if (declObj instanceof RecordDeclaration nestedRd) {
|
|
||||||
indexRecord(nestedRd, ownerFqn, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void classifyMethod(
|
|
||||||
String ownerFqn,
|
|
||||||
MethodDeclaration md,
|
|
||||||
TrivialAccessorDetector.FieldLookup fieldLookup,
|
|
||||||
DirectTypeAccessors state) {
|
|
||||||
if (md.getBody() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName = md.getName().getIdentifier();
|
|
||||||
Optional<AccessorSummary> summary = detector.detect(ownerFqn, md, fieldLookup);
|
|
||||||
applyClassification(state, methodName, summary);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyClassification(DirectTypeAccessors state, String methodName, Optional<AccessorSummary> summary) {
|
|
||||||
if (summary.isPresent()) {
|
|
||||||
state.trivial.put(methodName, summary.get());
|
|
||||||
state.blockedMethods.remove(methodName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (looksLikeAccessorMethod(methodName)) {
|
|
||||||
state.blockedMethods.add(methodName);
|
|
||||||
state.trivial.remove(methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean looksLikeAccessorMethod(String methodName) {
|
|
||||||
return methodName.startsWith("get")
|
|
||||||
|| methodName.startsWith("set")
|
|
||||||
|| methodName.startsWith("is");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, AccessorSummary> mergeEffective(
|
|
||||||
String ownerFqn,
|
|
||||||
Map<String, DirectTypeAccessors> direct,
|
|
||||||
Map<String, Map<String, AccessorSummary>> cache,
|
|
||||||
Set<String> visiting) {
|
|
||||||
ownerFqn = resolveTypeFqn(ownerFqn);
|
|
||||||
if (ownerFqn == null) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
if (cache.containsKey(ownerFqn)) {
|
|
||||||
return cache.get(ownerFqn);
|
|
||||||
}
|
|
||||||
if (!visiting.add(ownerFqn)) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, AccessorSummary> merged = new HashMap<>();
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (td != null) {
|
|
||||||
String superFqn = resolveTypeFqn(context.getSuperclassFqn(td));
|
|
||||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
|
||||||
for (Map.Entry<String, AccessorSummary> inherited : mergeEffective(superFqn, direct, cache, visiting).entrySet()) {
|
|
||||||
merged.put(inherited.getKey(), rebindOwner(inherited.getValue(), ownerFqn));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DirectTypeAccessors own = direct.get(ownerFqn);
|
|
||||||
if (own != null) {
|
|
||||||
own.blockedMethods.forEach(merged::remove);
|
|
||||||
merged.putAll(own.trivial);
|
|
||||||
}
|
|
||||||
|
|
||||||
visiting.remove(ownerFqn);
|
|
||||||
cache.put(ownerFqn, Map.copyOf(merged));
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AccessorSummary rebindOwner(AccessorSummary summary, String ownerFqn) {
|
|
||||||
if (summary.ownerFqn().equals(ownerFqn)) {
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
return new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
summary.methodName(),
|
|
||||||
summary.kind(),
|
|
||||||
summary.fieldName(),
|
|
||||||
summary.paramName(),
|
|
||||||
summary.declaringFqn(),
|
|
||||||
summary.returnsThis()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveTypeFqn(String typeName) {
|
|
||||||
if (typeName == null || typeName.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeName.contains(".")) {
|
|
||||||
return typeName;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(typeName);
|
|
||||||
return td != null ? context.getFqn(td) : typeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class DirectTypeAccessors {
|
|
||||||
private final Map<String, AccessorSummary> trivial = new HashMap<>();
|
|
||||||
private final Set<String> blockedMethods = new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> findFieldDeclaringFqn(TypeDeclaration td, String fieldName) {
|
|
||||||
return findFieldDeclaringFqn(td, fieldName, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> findFieldDeclaringFqn(TypeDeclaration td, String fieldName, Set<String> visited) {
|
|
||||||
if (td == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
String ownerFqn = context.getFqn(td);
|
|
||||||
if (!visited.add(ownerFqn)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
|
||||||
if (frag.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
return Optional.of(ownerFqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
|
||||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
if (superTd != null) {
|
|
||||||
return findFieldDeclaringFqn(superTd, fieldName, visited);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> findEnumFieldDeclaringFqn(EnumDeclaration ed, String ownerFqn, String fieldName) {
|
|
||||||
for (Object decl : ed.bodyDeclarations()) {
|
|
||||||
if (decl instanceof FieldDeclaration fd) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
|
||||||
if (frag.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
return Optional.of(ownerFqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> findRecordFieldDeclaringFqn(RecordDeclaration rd, String ownerFqn, String fieldName) {
|
|
||||||
for (Object compObj : rd.recordComponents()) {
|
|
||||||
String componentName = recordComponentName(compObj);
|
|
||||||
if (fieldName.equals(componentName)) {
|
|
||||||
return Optional.of(ownerFqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String recordComponentName(Object compObj) {
|
|
||||||
if (compObj instanceof SingleVariableDeclaration svd) {
|
|
||||||
return svd.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
java.lang.reflect.Method getNameMethod = compObj.getClass().getMethod("getName");
|
|
||||||
Object nameObj = getNameMethod.invoke(compObj);
|
|
||||||
if (nameObj instanceof SimpleName sn) {
|
|
||||||
return sn.getIdentifier();
|
|
||||||
}
|
|
||||||
} catch (ReflectiveOperationException ignored) {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.IMethodBinding;
|
|
||||||
import org.eclipse.jdt.core.dom.ITypeBinding;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public final class AccessorInlining {
|
|
||||||
|
|
||||||
private AccessorInlining() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Optional<AccessorSummary> lookupAccessor(
|
|
||||||
CodebaseContext context,
|
|
||||||
MethodInvocation mi,
|
|
||||||
List<Expression> receiverDefinitions) {
|
|
||||||
if (context == null || mi == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
if (isLikelyNonBeanGet(methodName, mi)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiverDefinitions != null) {
|
|
||||||
for (Expression receiverDef : receiverDefinitions) {
|
|
||||||
if (receiverDef instanceof ClassInstanceCreation cic) {
|
|
||||||
Optional<AccessorSummary> concrete = lookupOnType(context, resolveTypeFqn(cic), methodName);
|
|
||||||
if (concrete.isPresent()) {
|
|
||||||
return concrete;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
|
||||||
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
|
|
||||||
ITypeBinding declaringClass = methodBinding.getDeclaringClass().getErasure();
|
|
||||||
Optional<AccessorSummary> declared = lookupOnType(context, declaringClass.getQualifiedName(), methodName);
|
|
||||||
if (declared.isPresent()) {
|
|
||||||
return declared;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Optional<AccessorSummary> lookupAccessor(CodebaseContext context, MethodInvocation mi) {
|
|
||||||
return lookupAccessor(context, mi, List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<AccessorSummary> lookupOnType(CodebaseContext context, String ownerFqn, String methodName) {
|
|
||||||
if (ownerFqn == null || ownerFqn.isBlank() || ownerFqn.startsWith("java.")) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return context.getAccessorIndex().lookup(ownerFqn, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String resolveTypeFqn(ClassInstanceCreation cic) {
|
|
||||||
if (cic == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (cic.resolveConstructorBinding() != null && cic.resolveConstructorBinding().getDeclaringClass() != null) {
|
|
||||||
return cic.resolveConstructorBinding().getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
if (cic.resolveTypeBinding() != null) {
|
|
||||||
return cic.resolveTypeBinding().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
return AstUtils.extractSimpleTypeName(cic.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isLikelyNonBeanGet(String methodName, MethodInvocation mi) {
|
|
||||||
if (!"get".equals(methodName) && !"getOrDefault".equals(methodName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (mi.arguments().size() != 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
|
||||||
if (methodBinding == null || methodBinding.getDeclaringClass() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String ownerFqn = methodBinding.getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
return ownerFqn != null && ownerFqn.startsWith("java.util");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
public enum AccessorKind {
|
|
||||||
GETTER,
|
|
||||||
BOOLEAN_GETTER,
|
|
||||||
CONSTANT_GETTER,
|
|
||||||
RECORD_COMPONENT,
|
|
||||||
SETTER,
|
|
||||||
FLUENT_SETTER
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared JavaBean accessor naming helpers used by analysis and dataflow code.
|
|
||||||
*/
|
|
||||||
public final class AccessorNaming {
|
|
||||||
|
|
||||||
private AccessorNaming() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isBeanStyleAccessorName(String accessorName) {
|
|
||||||
return (accessorName.startsWith("get") && accessorName.length() > 3)
|
|
||||||
|| (accessorName.startsWith("is") && accessorName.length() > 2)
|
|
||||||
|| (accessorName.startsWith("set") && accessorName.length() > 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String propertyNameFromAccessor(String accessorName) {
|
|
||||||
if (accessorName.startsWith("get") && accessorName.length() > 3) {
|
|
||||||
return TrivialAccessorDetector.decapitalize(accessorName.substring(3));
|
|
||||||
}
|
|
||||||
if (accessorName.startsWith("is") && accessorName.length() > 2) {
|
|
||||||
return TrivialAccessorDetector.decapitalize(accessorName.substring(2));
|
|
||||||
}
|
|
||||||
if (accessorName.startsWith("set") && accessorName.length() > 3) {
|
|
||||||
return TrivialAccessorDetector.decapitalize(accessorName.substring(3));
|
|
||||||
}
|
|
||||||
return accessorName.startsWith("get") ? accessorName.substring(3) : accessorName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String setterMethodNameForGetter(String getterName) {
|
|
||||||
return "set" + capitalizeProperty(propertyNameFromAccessor(getterName));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String capitalizeProperty(String propertyName) {
|
|
||||||
if (propertyName == null || propertyName.isEmpty()) {
|
|
||||||
return propertyName;
|
|
||||||
}
|
|
||||||
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
|
|
||||||
return propertyName;
|
|
||||||
}
|
|
||||||
return Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean looksLikeIndexedAccessorName(String methodName) {
|
|
||||||
return methodName.startsWith("get") || methodName.startsWith("is") || methodName.startsWith("set");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
public record AccessorSummary(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
AccessorKind kind,
|
|
||||||
String fieldName,
|
|
||||||
String paramName,
|
|
||||||
String declaringFqn,
|
|
||||||
boolean returnsThis
|
|
||||||
) {
|
|
||||||
public boolean isGetter() {
|
|
||||||
return kind == AccessorKind.GETTER
|
|
||||||
|| kind == AccessorKind.BOOLEAN_GETTER
|
|
||||||
|| kind == AccessorKind.CONSTANT_GETTER
|
|
||||||
|| kind == AccessorKind.RECORD_COMPONENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSetter() {
|
|
||||||
return kind == AccessorKind.SETTER || kind == AccessorKind.FLUENT_SETTER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public final class FieldTypeIndex {
|
|
||||||
|
|
||||||
private static final FieldTypeIndex EMPTY = new FieldTypeIndex(Map.of(), 0);
|
|
||||||
|
|
||||||
private final Map<String, Map<String, String>> byOwnerAndField;
|
|
||||||
private final int fieldCount;
|
|
||||||
|
|
||||||
public FieldTypeIndex(Map<String, Map<String, String>> byOwnerAndField, int fieldCount) {
|
|
||||||
this.byOwnerAndField = Collections.unmodifiableMap(deepCopy(byOwnerAndField));
|
|
||||||
this.fieldCount = fieldCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FieldTypeIndex empty() {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> lookup(String ownerFqn, String fieldName) {
|
|
||||||
if (ownerFqn == null || fieldName == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Map<String, String> fields = byOwnerAndField.get(ownerFqn);
|
|
||||||
if (fields == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.ofNullable(fields.get(fieldName));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int fieldCount() {
|
|
||||||
return fieldCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Map<String, String>> deepCopy(Map<String, Map<String, String>> source) {
|
|
||||||
Map<String, Map<String, String>> copy = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Map<String, String>> entry : source.entrySet()) {
|
|
||||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.Modifier;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public final class FieldTypeIndexBuilder {
|
|
||||||
|
|
||||||
private CodebaseContext context;
|
|
||||||
|
|
||||||
public FieldTypeIndex build(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
if (context == null) {
|
|
||||||
return FieldTypeIndex.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Map<String, String>> direct = new HashMap<>();
|
|
||||||
for (String ownerFqn : context.getIndexedTypeFqns()) {
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (typeDeclaration != null) {
|
|
||||||
indexDeclaredFields(ownerFqn, typeDeclaration, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Map<String, String>> effective = new HashMap<>();
|
|
||||||
int fieldCount = 0;
|
|
||||||
Map<String, Map<String, String>> cache = new HashMap<>();
|
|
||||||
for (String ownerFqn : direct.keySet()) {
|
|
||||||
Map<String, String> merged = mergeEffective(ownerFqn, direct, cache, new HashSet<>());
|
|
||||||
if (!merged.isEmpty()) {
|
|
||||||
effective.put(ownerFqn, merged);
|
|
||||||
fieldCount += merged.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Built field-type index: {} fields across {} types", fieldCount, effective.size());
|
|
||||||
this.context = null;
|
|
||||||
return new FieldTypeIndex(effective, fieldCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexDeclaredFields(
|
|
||||||
String ownerFqn,
|
|
||||||
TypeDeclaration typeDeclaration,
|
|
||||||
Map<String, Map<String, String>> direct) {
|
|
||||||
Map<String, String> fields = direct.computeIfAbsent(ownerFqn, ignored -> new HashMap<>());
|
|
||||||
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
|
||||||
String fieldType = fieldDeclaration.getType().toString();
|
|
||||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
|
||||||
fields.put(fragment.getName().getIdentifier(), fieldType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (TypeDeclaration nested : typeDeclaration.getTypes()) {
|
|
||||||
indexDeclaredFields(context.getFqn(nested), nested, direct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> mergeEffective(
|
|
||||||
String ownerFqn,
|
|
||||||
Map<String, Map<String, String>> direct,
|
|
||||||
Map<String, Map<String, String>> cache,
|
|
||||||
Set<String> visiting) {
|
|
||||||
if (cache.containsKey(ownerFqn)) {
|
|
||||||
return cache.get(ownerFqn);
|
|
||||||
}
|
|
||||||
if (!visiting.add(ownerFqn)) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> merged = new HashMap<>();
|
|
||||||
TypeDeclaration ownerType = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (ownerType != null) {
|
|
||||||
String superFqn = context.getSuperclassFqn(ownerType);
|
|
||||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
|
||||||
merged.putAll(mergeEffective(superFqn, direct, cache, visiting));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> own = direct.get(ownerFqn);
|
|
||||||
if (own != null) {
|
|
||||||
merged.putAll(own);
|
|
||||||
}
|
|
||||||
|
|
||||||
visiting.remove(ownerFqn);
|
|
||||||
cache.put(ownerFqn, Map.copyOf(merged));
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan-time result for {@code owner.getX()} getter-chain hops: the next receiver type
|
|
||||||
* and optional {@code new ...()} field initializer discovered on the backing field.
|
|
||||||
*/
|
|
||||||
public record GetterChainEdge(
|
|
||||||
String ownerFqn,
|
|
||||||
String getterName,
|
|
||||||
String nextTypeFqn,
|
|
||||||
ClassInstanceCreation fieldInitializerCic) {
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public final class GetterChainIndex {
|
|
||||||
|
|
||||||
private static final GetterChainIndex EMPTY = new GetterChainIndex(Map.of(), 0);
|
|
||||||
|
|
||||||
private final Map<String, Map<String, GetterChainEdge>> byOwnerAndGetter;
|
|
||||||
private final int edgeCount;
|
|
||||||
|
|
||||||
public GetterChainIndex(Map<String, Map<String, GetterChainEdge>> byOwnerAndGetter, int edgeCount) {
|
|
||||||
this.byOwnerAndGetter = Collections.unmodifiableMap(deepCopy(byOwnerAndGetter));
|
|
||||||
this.edgeCount = edgeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GetterChainIndex empty() {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<GetterChainEdge> lookup(String ownerFqn, String getterName) {
|
|
||||||
if (ownerFqn == null || getterName == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Map<String, GetterChainEdge> getters = byOwnerAndGetter.get(ownerFqn);
|
|
||||||
if (getters == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.ofNullable(getters.get(getterName));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int edgeCount() {
|
|
||||||
return edgeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Map<String, GetterChainEdge>> deepCopy(
|
|
||||||
Map<String, Map<String, GetterChainEdge>> source) {
|
|
||||||
Map<String, Map<String, GetterChainEdge>> copy = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Map<String, GetterChainEdge>> entry : source.entrySet()) {
|
|
||||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public final class GetterChainIndexBuilder {
|
|
||||||
|
|
||||||
public GetterChainIndex build(CodebaseContext context, AccessorIndex accessorIndex) {
|
|
||||||
if (context == null || accessorIndex == null) {
|
|
||||||
return GetterChainIndex.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldInitializerFinder fieldInitializerFinder = new FieldInitializerFinder(context);
|
|
||||||
Map<String, Map<String, GetterChainEdge>> edges = new HashMap<>();
|
|
||||||
int edgeCount = 0;
|
|
||||||
|
|
||||||
for (Map.Entry<String, Map<String, AccessorSummary>> ownerEntry : accessorIndex.asMap().entrySet()) {
|
|
||||||
String ownerFqn = ownerEntry.getKey();
|
|
||||||
for (AccessorSummary accessor : ownerEntry.getValue().values()) {
|
|
||||||
if (!accessor.isGetter()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
GetterChainEdge edge = buildEdge(context, fieldInitializerFinder, ownerFqn, accessor);
|
|
||||||
if (edge == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
edges.computeIfAbsent(ownerFqn, ignored -> new HashMap<>())
|
|
||||||
.put(accessor.methodName(), edge);
|
|
||||||
edgeCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Built getter-chain index: {} edges across {} types", edgeCount, edges.size());
|
|
||||||
return new GetterChainIndex(edges, edgeCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GetterChainEdge buildEdge(
|
|
||||||
CodebaseContext context,
|
|
||||||
FieldInitializerFinder fieldInitializerFinder,
|
|
||||||
String ownerFqn,
|
|
||||||
AccessorSummary accessor) {
|
|
||||||
String nextType = null;
|
|
||||||
ClassInstanceCreation fieldInitializerCic = null;
|
|
||||||
|
|
||||||
if (accessor.fieldName() != null) {
|
|
||||||
fieldInitializerCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, accessor.fieldName());
|
|
||||||
nextType = fieldInitializerFinder.resolveFieldTypeName(ownerFqn, accessor.fieldName());
|
|
||||||
if (fieldInitializerCic != null) {
|
|
||||||
nextType = AstUtils.extractSimpleTypeName(fieldInitializerCic.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextType == null) {
|
|
||||||
TypeDeclaration ownerType = context.getTypeDeclaration(ownerFqn);
|
|
||||||
MethodDeclaration getter = ownerType != null
|
|
||||||
? context.findMethodDeclaration(ownerType, accessor.methodName(), true)
|
|
||||||
: null;
|
|
||||||
if (getter != null && getter.getReturnType2() != null) {
|
|
||||||
nextType = getter.getReturnType2().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextType == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new GetterChainEdge(ownerFqn, accessor.methodName(), nextType, fieldInitializerCic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan-time constants for indexed getters backed by fields with literal or enum initializers.
|
|
||||||
*/
|
|
||||||
public final class IndexedFieldConstantsIndex {
|
|
||||||
|
|
||||||
private static final IndexedFieldConstantsIndex EMPTY = new IndexedFieldConstantsIndex(Map.of(), 0);
|
|
||||||
|
|
||||||
private final Map<String, Map<String, List<String>>> byOwnerAndMethod;
|
|
||||||
private final int entryCount;
|
|
||||||
|
|
||||||
public IndexedFieldConstantsIndex(Map<String, Map<String, List<String>>> byOwnerAndMethod, int entryCount) {
|
|
||||||
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
|
|
||||||
this.entryCount = entryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IndexedFieldConstantsIndex empty() {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<List<String>> lookup(String ownerFqn, String methodName) {
|
|
||||||
if (ownerFqn == null || methodName == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Map<String, List<String>> methods = byOwnerAndMethod.get(ownerFqn);
|
|
||||||
if (methods == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
List<String> constants = methods.get(methodName);
|
|
||||||
if (constants == null || constants.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(constants);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int entryCount() {
|
|
||||||
return entryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Map<String, List<String>>> deepCopy(Map<String, Map<String, List<String>>> source) {
|
|
||||||
Map<String, Map<String, List<String>>> copy = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Map<String, List<String>>> entry : source.entrySet()) {
|
|
||||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public final class IndexedFieldConstantsIndexBuilder {
|
|
||||||
|
|
||||||
public IndexedFieldConstantsIndex build(CodebaseContext context, AccessorIndex accessorIndex) {
|
|
||||||
if (context == null || accessorIndex == null) {
|
|
||||||
return IndexedFieldConstantsIndex.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Map<String, List<String>>> entries = new HashMap<>();
|
|
||||||
int entryCount = 0;
|
|
||||||
|
|
||||||
for (Map.Entry<String, Map<String, AccessorSummary>> ownerEntry : accessorIndex.asMap().entrySet()) {
|
|
||||||
String ownerFqn = ownerEntry.getKey();
|
|
||||||
for (AccessorSummary accessor : ownerEntry.getValue().values()) {
|
|
||||||
if (!accessor.isGetter() || accessor.fieldName() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<String> constants = ScanTimeAccessorConstants.resolve(
|
|
||||||
context, accessorIndex, ownerFqn, accessor.methodName());
|
|
||||||
if (constants.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entries.computeIfAbsent(ownerFqn, ignored -> new HashMap<>())
|
|
||||||
.put(accessor.methodName(), List.copyOf(constants));
|
|
||||||
entryCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Built indexed field-constants index: {} entries across {} types", entryCount, entries.size());
|
|
||||||
return new IndexedFieldConstantsIndex(entries, entryCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan-time summary of how a getter on an interface or abstract type resolves across
|
|
||||||
* its concrete implementations. Values are derived from static analysis only.
|
|
||||||
*/
|
|
||||||
public record PolymorphicAccessorEntry(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
Optional<AccessorSummary> representativeImplAccessor,
|
|
||||||
List<String> widenedReturnConstants,
|
|
||||||
boolean staticallyComplete) {
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public final class PolymorphicAccessorIndex {
|
|
||||||
|
|
||||||
private static final PolymorphicAccessorIndex EMPTY = new PolymorphicAccessorIndex(Map.of(), 0);
|
|
||||||
|
|
||||||
private final Map<String, Map<String, PolymorphicAccessorEntry>> byOwnerAndMethod;
|
|
||||||
private final int entryCount;
|
|
||||||
|
|
||||||
public PolymorphicAccessorIndex(
|
|
||||||
Map<String, Map<String, PolymorphicAccessorEntry>> byOwnerAndMethod,
|
|
||||||
int entryCount) {
|
|
||||||
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
|
|
||||||
this.entryCount = entryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PolymorphicAccessorIndex empty() {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<PolymorphicAccessorEntry> lookup(String ownerFqn, String methodName) {
|
|
||||||
if (ownerFqn == null || methodName == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Map<String, PolymorphicAccessorEntry> methods = byOwnerAndMethod.get(ownerFqn);
|
|
||||||
if (methods == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.ofNullable(methods.get(methodName));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> representativeImplAccessor(String ownerFqn, String methodName) {
|
|
||||||
return lookup(ownerFqn, methodName)
|
|
||||||
.flatMap(PolymorphicAccessorEntry::representativeImplAccessor);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<List<String>> widenedReturnConstants(String ownerFqn, String methodName) {
|
|
||||||
return lookup(ownerFqn, methodName)
|
|
||||||
.filter(PolymorphicAccessorEntry::staticallyComplete)
|
|
||||||
.map(PolymorphicAccessorEntry::widenedReturnConstants)
|
|
||||||
.filter(values -> !values.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public int entryCount() {
|
|
||||||
return entryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Map<String, PolymorphicAccessorEntry>> deepCopy(
|
|
||||||
Map<String, Map<String, PolymorphicAccessorEntry>> source) {
|
|
||||||
Map<String, Map<String, PolymorphicAccessorEntry>> copy = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Map<String, PolymorphicAccessorEntry>> entry : source.entrySet()) {
|
|
||||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.Modifier;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public final class PolymorphicAccessorIndexBuilder {
|
|
||||||
|
|
||||||
public PolymorphicAccessorIndex build(CodebaseContext context, AccessorIndex accessorIndex) {
|
|
||||||
if (context == null || accessorIndex == null) {
|
|
||||||
return PolymorphicAccessorIndex.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Map<String, PolymorphicAccessorEntry>> entries = new HashMap<>();
|
|
||||||
int entryCount = 0;
|
|
||||||
|
|
||||||
for (String typeFqn : context.getIndexedTypeFqns()) {
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(typeFqn);
|
|
||||||
if (!isPolymorphicType(typeDeclaration)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<String> implementations = context.getImplementations(typeFqn);
|
|
||||||
if (implementations.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> methodNames = collectGetterMethodNames(context, accessorIndex, typeFqn, implementations);
|
|
||||||
for (String methodName : methodNames) {
|
|
||||||
PolymorphicAccessorEntry entry = buildEntry(
|
|
||||||
context, accessorIndex, typeFqn, methodName, implementations);
|
|
||||||
if (entry == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entries.computeIfAbsent(typeFqn, ignored -> new HashMap<>()).put(methodName, entry);
|
|
||||||
entryCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Built polymorphic accessor index: {} entries across {} types", entryCount, entries.size());
|
|
||||||
return new PolymorphicAccessorIndex(entries, entryCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PolymorphicAccessorEntry buildEntry(
|
|
||||||
CodebaseContext context,
|
|
||||||
AccessorIndex accessorIndex,
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
List<String> implementations) {
|
|
||||||
LinkedHashSet<String> widenedConstants = new LinkedHashSet<>();
|
|
||||||
widenedConstants.addAll(ScanTimeAccessorConstants.resolve(context, accessorIndex, ownerFqn, methodName));
|
|
||||||
|
|
||||||
Optional<AccessorSummary> representative = findRepresentativeImplAccessor(accessorIndex, implementations, methodName);
|
|
||||||
for (String implementationFqn : implementations) {
|
|
||||||
widenedConstants.addAll(
|
|
||||||
ScanTimeAccessorConstants.resolve(context, accessorIndex, implementationFqn, methodName));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (representative.isEmpty() && widenedConstants.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
boolean staticallyComplete = isStaticallyComplete(accessorIndex, methodName, implementations);
|
|
||||||
return new PolymorphicAccessorEntry(
|
|
||||||
ownerFqn,
|
|
||||||
methodName,
|
|
||||||
representative,
|
|
||||||
List.copyOf(widenedConstants),
|
|
||||||
staticallyComplete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isStaticallyComplete(
|
|
||||||
AccessorIndex accessorIndex,
|
|
||||||
String methodName,
|
|
||||||
List<String> implementations) {
|
|
||||||
for (String implementationFqn : implementations) {
|
|
||||||
if (!isConstantLikeAccessor(accessorIndex, implementationFqn, methodName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isConstantLikeAccessor(
|
|
||||||
AccessorIndex accessorIndex,
|
|
||||||
String typeFqn,
|
|
||||||
String methodName) {
|
|
||||||
Optional<AccessorSummary> accessor = accessorIndex.lookup(typeFqn, methodName);
|
|
||||||
if (accessor.isEmpty() || !accessor.get().isGetter()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return accessor.get().kind() == AccessorKind.CONSTANT_GETTER;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<AccessorSummary> findRepresentativeImplAccessor(
|
|
||||||
AccessorIndex accessorIndex,
|
|
||||||
List<String> implementations,
|
|
||||||
String methodName) {
|
|
||||||
for (String implementationFqn : implementations) {
|
|
||||||
Optional<AccessorSummary> implAccessor = accessorIndex.lookup(implementationFqn, methodName);
|
|
||||||
if (implAccessor.isPresent() && implAccessor.get().isGetter()) {
|
|
||||||
return implAccessor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> collectGetterMethodNames(
|
|
||||||
CodebaseContext context,
|
|
||||||
AccessorIndex accessorIndex,
|
|
||||||
String ownerFqn,
|
|
||||||
List<String> implementations) {
|
|
||||||
Set<String> methodNames = new LinkedHashSet<>();
|
|
||||||
for (AccessorSummary accessor : accessorIndex.accessorsForType(ownerFqn)) {
|
|
||||||
if (accessor.isGetter()) {
|
|
||||||
methodNames.add(accessor.methodName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TypeDeclaration ownerType = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (ownerType != null) {
|
|
||||||
for (MethodDeclaration method : ownerType.getMethods()) {
|
|
||||||
String name = method.getName().getIdentifier();
|
|
||||||
if (looksLikeGetterName(name)) {
|
|
||||||
methodNames.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (String implementationFqn : implementations) {
|
|
||||||
for (AccessorSummary accessor : accessorIndex.accessorsForType(implementationFqn)) {
|
|
||||||
if (accessor.isGetter()) {
|
|
||||||
methodNames.add(accessor.methodName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return methodNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPolymorphicType(TypeDeclaration typeDeclaration) {
|
|
||||||
return typeDeclaration != null
|
|
||||||
&& (typeDeclaration.isInterface()
|
|
||||||
|| Modifier.isAbstract(typeDeclaration.getModifiers()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean looksLikeGetterName(String methodName) {
|
|
||||||
return methodName.startsWith("get") || methodName.startsWith("is");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan-time expansion of interface/abstract method calls to concrete implementation method FQNs.
|
|
||||||
*/
|
|
||||||
public final class PolymorphicMethodExpansionIndex {
|
|
||||||
|
|
||||||
private static final PolymorphicMethodExpansionIndex EMPTY =
|
|
||||||
new PolymorphicMethodExpansionIndex(Map.of(), 0);
|
|
||||||
|
|
||||||
private final Map<String, Map<String, List<String>>> byOwnerAndMethod;
|
|
||||||
private final int entryCount;
|
|
||||||
|
|
||||||
public PolymorphicMethodExpansionIndex(
|
|
||||||
Map<String, Map<String, List<String>>> byOwnerAndMethod,
|
|
||||||
int entryCount) {
|
|
||||||
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
|
|
||||||
this.entryCount = entryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PolymorphicMethodExpansionIndex empty() {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<List<String>> lookup(String ownerFqn, String methodName) {
|
|
||||||
if (ownerFqn == null || methodName == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Map<String, List<String>> methods = byOwnerAndMethod.get(ownerFqn);
|
|
||||||
if (methods == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
List<String> expanded = methods.get(methodName);
|
|
||||||
if (expanded == null || expanded.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(expanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int entryCount() {
|
|
||||||
return entryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, Map<String, List<String>>> deepCopy(
|
|
||||||
Map<String, Map<String, List<String>>> source) {
|
|
||||||
Map<String, Map<String, List<String>>> copy = new HashMap<>();
|
|
||||||
for (Map.Entry<String, Map<String, List<String>>> entry : source.entrySet()) {
|
|
||||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.jdt.core.dom.Modifier;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public final class PolymorphicMethodExpansionIndexBuilder {
|
|
||||||
|
|
||||||
public PolymorphicMethodExpansionIndex build(CodebaseContext context) {
|
|
||||||
if (context == null) {
|
|
||||||
return PolymorphicMethodExpansionIndex.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Map<String, List<String>>> entries = new HashMap<>();
|
|
||||||
int entryCount = 0;
|
|
||||||
|
|
||||||
for (String typeFqn : context.getIndexedTypeFqns()) {
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(typeFqn);
|
|
||||||
if (!isPolymorphicType(typeDeclaration)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<String> implementations = context.getImplementations(typeFqn);
|
|
||||||
if (implementations.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> methodNames = collectMethodNames(context, typeDeclaration, implementations);
|
|
||||||
for (String methodName : methodNames) {
|
|
||||||
List<String> expanded = new ArrayList<>();
|
|
||||||
for (String implementationFqn : implementations) {
|
|
||||||
expanded.add(implementationFqn + "." + methodName);
|
|
||||||
}
|
|
||||||
entries.computeIfAbsent(typeFqn, ignored -> new HashMap<>())
|
|
||||||
.put(methodName, List.copyOf(expanded));
|
|
||||||
entryCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Built polymorphic method expansion index: {} entries across {} types",
|
|
||||||
entryCount, entries.size());
|
|
||||||
return new PolymorphicMethodExpansionIndex(entries, entryCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> collectMethodNames(
|
|
||||||
CodebaseContext context,
|
|
||||||
TypeDeclaration ownerType,
|
|
||||||
List<String> implementations) {
|
|
||||||
Set<String> methodNames = new LinkedHashSet<>();
|
|
||||||
if (ownerType != null) {
|
|
||||||
for (var method : ownerType.getMethods()) {
|
|
||||||
methodNames.add(method.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (String implementationFqn : implementations) {
|
|
||||||
TypeDeclaration implementationType = context.getTypeDeclaration(implementationFqn);
|
|
||||||
if (implementationType == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (var method : implementationType.getMethods()) {
|
|
||||||
methodNames.add(method.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return methodNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPolymorphicType(TypeDeclaration typeDeclaration) {
|
|
||||||
return typeDeclaration != null
|
|
||||||
&& (typeDeclaration.isInterface()
|
|
||||||
|| Modifier.isAbstract(typeDeclaration.getModifiers()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves accessor return constants using only scan-time information: indexed accessors,
|
|
||||||
* literal method bodies, and field initializers. No dataflow or constructor tracing.
|
|
||||||
*/
|
|
||||||
public final class ScanTimeAccessorConstants {
|
|
||||||
|
|
||||||
private ScanTimeAccessorConstants() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> resolve(
|
|
||||||
CodebaseContext context,
|
|
||||||
AccessorIndex accessorIndex,
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName) {
|
|
||||||
if (context == null || accessorIndex == null || ownerFqn == null || methodName == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
Set<String> deduped = new LinkedHashSet<>();
|
|
||||||
Optional<AccessorSummary> indexed = accessorIndex.lookup(ownerFqn, methodName);
|
|
||||||
if (indexed.isPresent() && indexed.get().isGetter()) {
|
|
||||||
if (indexed.get().kind() == AccessorKind.CONSTANT_GETTER) {
|
|
||||||
deduped.addAll(extractReturnConstants(context, ownerFqn, methodName));
|
|
||||||
} else if (indexed.get().fieldName() != null) {
|
|
||||||
deduped.addAll(extractFieldInitializerConstants(
|
|
||||||
context, ownerFqn, indexed.get().fieldName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (deduped.isEmpty()) {
|
|
||||||
deduped.addAll(extractReturnConstants(context, ownerFqn, methodName));
|
|
||||||
}
|
|
||||||
return List.copyOf(deduped);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> extractReturnConstants(
|
|
||||||
CodebaseContext context,
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName) {
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
|
||||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
ConstantResolver constantResolver = context.getConstantResolver();
|
|
||||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
Expression expression = node.getExpression();
|
|
||||||
if (expression != null) {
|
|
||||||
addResolvedExpression(constantResolver, context, expression, constants);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> extractFieldInitializerConstants(
|
|
||||||
CodebaseContext context,
|
|
||||||
String ownerFqn,
|
|
||||||
String fieldName) {
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
collectFieldInitializerConstants(context, typeDeclaration, fieldName, constants);
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collectFieldInitializerConstants(
|
|
||||||
CodebaseContext context,
|
|
||||||
TypeDeclaration typeDeclaration,
|
|
||||||
String fieldName,
|
|
||||||
List<String> constants) {
|
|
||||||
ConstantResolver constantResolver = context.getConstantResolver();
|
|
||||||
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
|
||||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
|
||||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(fieldName)
|
|
||||||
&& fragment.getInitializer() != null) {
|
|
||||||
addResolvedExpression(
|
|
||||||
constantResolver, context, fragment.getInitializer(), constants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (constants.isEmpty()) {
|
|
||||||
String superFqn = context.getSuperclassFqn(typeDeclaration);
|
|
||||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
|
||||||
TypeDeclaration superType = context.getTypeDeclaration(superFqn);
|
|
||||||
if (superType != null) {
|
|
||||||
collectFieldInitializerConstants(context, superType, fieldName, constants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addResolvedExpression(
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
CodebaseContext context,
|
|
||||||
Expression expression,
|
|
||||||
List<String> constants) {
|
|
||||||
if (constantResolver == null || expression == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String resolved = constantResolver.resolve(expression, context);
|
|
||||||
if (resolved == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (resolved.startsWith("ENUM_SET:")) {
|
|
||||||
for (String value : resolved.substring(9).split(",")) {
|
|
||||||
addUnique(constants, parseEnumSetElement(value));
|
|
||||||
}
|
|
||||||
} else if (resolved.startsWith("MAP:")) {
|
|
||||||
List<String> mapValues = ConstantResolver.decodeMapLiteralValues(resolved);
|
|
||||||
if (mapValues != null) {
|
|
||||||
for (String mapValue : mapValues) {
|
|
||||||
addUnique(constants, mapValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (resolved.startsWith("ARRAY:")) {
|
|
||||||
for (String arrayValue : resolved.substring(6).split("\\|", -1)) {
|
|
||||||
addUnique(constants, arrayValue);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addUnique(constants, resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String parseEnumSetElement(String value) {
|
|
||||||
int lastDot = value.lastIndexOf('.');
|
|
||||||
int secondLastDot = lastDot > 0 ? value.lastIndexOf('.', lastDot - 1) : -1;
|
|
||||||
return secondLastDot >= 0 ? value.substring(secondLastDot + 1) : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addUnique(List<String> constants, String value) {
|
|
||||||
if (value != null && !constants.contains(value)) {
|
|
||||||
constants.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.index;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects trivial JavaBean-style getters and setters whose bodies only read or write a single field.
|
|
||||||
*/
|
|
||||||
public class TrivialAccessorDetector {
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
public interface FieldLookup extends Function<String, Optional<String>> {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> detect(String ownerFqn, MethodDeclaration method, FieldLookup fieldLookup) {
|
|
||||||
if (ownerFqn == null || method == null || method.getBody() == null || fieldLookup == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName = method.getName().getIdentifier();
|
|
||||||
if (method.isConstructor()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<String> getterField = propertyNameForGetter(methodName, method);
|
|
||||||
if (getterField.isPresent()) {
|
|
||||||
Optional<AccessorSummary> fieldGetter = detectGetter(ownerFqn, method, methodName, getterField.get(), fieldLookup);
|
|
||||||
if (fieldGetter.isPresent()) {
|
|
||||||
return fieldGetter;
|
|
||||||
}
|
|
||||||
return detectConstantGetter(ownerFqn, method, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<String> setterField = propertyNameForSetter(methodName);
|
|
||||||
if (setterField.isPresent() && !method.parameters().isEmpty()) {
|
|
||||||
return detectSetter(ownerFqn, method, methodName, setterField.get(), fieldLookup);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> detectRecordComponent(String ownerFqn, String componentName) {
|
|
||||||
if (ownerFqn == null || componentName == null || componentName.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
componentName,
|
|
||||||
AccessorKind.RECORD_COMPONENT,
|
|
||||||
componentName,
|
|
||||||
null,
|
|
||||||
ownerFqn,
|
|
||||||
false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> detectConstantGetter(
|
|
||||||
String ownerFqn,
|
|
||||||
MethodDeclaration method,
|
|
||||||
String methodName) {
|
|
||||||
List<?> statements = method.getBody().statements();
|
|
||||||
if (statements.size() != 1 || !(statements.get(0) instanceof ReturnStatement rs)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Expression expr = rs.getExpression();
|
|
||||||
if (expr == null || !isConstantReturnExpression(expr)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
methodName,
|
|
||||||
AccessorKind.CONSTANT_GETTER,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
ownerFqn,
|
|
||||||
false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isConstantReturnExpression(Expression expr) {
|
|
||||||
Expression current = unwrap(expr);
|
|
||||||
if (current instanceof StringLiteral
|
|
||||||
|| current instanceof NumberLiteral
|
|
||||||
|| current instanceof BooleanLiteral
|
|
||||||
|| current instanceof CharacterLiteral) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (current instanceof QualifiedName) {
|
|
||||||
return !containsMethodCall(current);
|
|
||||||
}
|
|
||||||
if (current instanceof SimpleName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (current instanceof FieldAccess fa) {
|
|
||||||
Expression receiver = fa.getExpression();
|
|
||||||
return (receiver instanceof SimpleName || receiver instanceof QualifiedName)
|
|
||||||
&& !containsMethodCall(current);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> detectGetter(
|
|
||||||
String ownerFqn,
|
|
||||||
MethodDeclaration method,
|
|
||||||
String methodName,
|
|
||||||
String fieldName,
|
|
||||||
FieldLookup fieldLookup) {
|
|
||||||
if (!isTrivialGetterBody(method.getBody(), fieldName)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Optional<String> declaringFqn = fieldLookup.apply(fieldName);
|
|
||||||
if (declaringFqn.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessorKind kind = methodName.startsWith("is") ? AccessorKind.BOOLEAN_GETTER : AccessorKind.GETTER;
|
|
||||||
return Optional.of(new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
methodName,
|
|
||||||
kind,
|
|
||||||
fieldName,
|
|
||||||
null,
|
|
||||||
declaringFqn.get(),
|
|
||||||
false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> detectSetter(
|
|
||||||
String ownerFqn,
|
|
||||||
MethodDeclaration method,
|
|
||||||
String methodName,
|
|
||||||
String fieldName,
|
|
||||||
FieldLookup fieldLookup) {
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) method.parameters().get(0);
|
|
||||||
String paramName = param.getName().getIdentifier();
|
|
||||||
|
|
||||||
boolean fluent = isTrivialFluentSetterBody(method.getBody(), fieldName, paramName);
|
|
||||||
boolean voidSetter = isVoidReturn(method) && isTrivialVoidSetterBody(method.getBody(), fieldName, paramName);
|
|
||||||
if (!fluent && !voidSetter) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<String> declaringFqn = fieldLookup.apply(fieldName);
|
|
||||||
if (declaringFqn.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessorKind kind = fluent ? AccessorKind.FLUENT_SETTER : AccessorKind.SETTER;
|
|
||||||
return Optional.of(new AccessorSummary(
|
|
||||||
ownerFqn,
|
|
||||||
methodName,
|
|
||||||
kind,
|
|
||||||
fieldName,
|
|
||||||
paramName,
|
|
||||||
declaringFqn.get(),
|
|
||||||
fluent
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> propertyNameForGetter(String methodName, MethodDeclaration method) {
|
|
||||||
if (methodName.startsWith("get") && methodName.length() > 3) {
|
|
||||||
return Optional.of(decapitalize(methodName.substring(3)));
|
|
||||||
}
|
|
||||||
if (methodName.startsWith("is") && methodName.length() > 2 && isBooleanReturn(method)) {
|
|
||||||
return Optional.of(decapitalize(methodName.substring(2)));
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> propertyNameForSetter(String methodName) {
|
|
||||||
if (methodName.startsWith("set") && methodName.length() > 3) {
|
|
||||||
return Optional.of(decapitalize(methodName.substring(3)));
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isBooleanReturn(MethodDeclaration method) {
|
|
||||||
return "boolean".equals(method.getReturnType2().toString())
|
|
||||||
|| "Boolean".equals(method.getReturnType2().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isVoidReturn(MethodDeclaration method) {
|
|
||||||
return method.getReturnType2() instanceof PrimitiveType pt && pt.getPrimitiveTypeCode() == PrimitiveType.VOID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String decapitalize(String name) {
|
|
||||||
if (name == null || name.isEmpty()) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
if (name.length() > 1 && Character.isUpperCase(name.charAt(1))) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTrivialGetterBody(Block body, String fieldName) {
|
|
||||||
List<?> statements = body.statements();
|
|
||||||
if (statements.size() != 1 || !(statements.get(0) instanceof ReturnStatement rs)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Expression expr = rs.getExpression();
|
|
||||||
if (expr == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isDirectFieldRead(unwrap(expr), fieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTrivialVoidSetterBody(Block body, String fieldName, String paramName) {
|
|
||||||
List<?> statements = body.statements();
|
|
||||||
if (statements.size() != 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isAssignmentStatement(statements.get(0), fieldName, paramName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTrivialFluentSetterBody(Block body, String fieldName, String paramName) {
|
|
||||||
List<?> statements = body.statements();
|
|
||||||
if (statements.size() != 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isAssignmentStatement(statements.get(0), fieldName, paramName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!(statements.get(1) instanceof ReturnStatement rs)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Expression ret = unwrap(rs.getExpression());
|
|
||||||
return ret instanceof ThisExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAssignmentStatement(Object statement, String fieldName, String paramName) {
|
|
||||||
if (!(statement instanceof ExpressionStatement es)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!(es.getExpression() instanceof Assignment assignment)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (assignment.getOperator() != Assignment.Operator.ASSIGN) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isDirectFieldWrite(assignment.getLeftHandSide(), fieldName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Expression rhs = unwrap(assignment.getRightHandSide());
|
|
||||||
return rhs instanceof SimpleName sn && sn.getIdentifier().equals(paramName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDirectFieldRead(Expression expr, String fieldName) {
|
|
||||||
if (containsMethodCall(expr)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
return sn.getIdentifier().equals(fieldName);
|
|
||||||
}
|
|
||||||
if (expr instanceof FieldAccess fa) {
|
|
||||||
Expression receiver = fa.getExpression();
|
|
||||||
if (receiver == null || receiver instanceof ThisExpression) {
|
|
||||||
return fa.getName().getIdentifier().equals(fieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDirectFieldWrite(Expression expr, String fieldName) {
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
return sn.getIdentifier().equals(fieldName);
|
|
||||||
}
|
|
||||||
if (expr instanceof FieldAccess fa) {
|
|
||||||
Expression receiver = fa.getExpression();
|
|
||||||
if (receiver == null || receiver instanceof ThisExpression) {
|
|
||||||
return fa.getName().getIdentifier().equals(fieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean containsMethodCall(Expression expr) {
|
|
||||||
if (expr == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final boolean[] found = {false};
|
|
||||||
expr.accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
found[0] = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return found[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expression unwrap(Expression expr) {
|
|
||||||
Expression current = expr;
|
|
||||||
while (current instanceof ParenthesizedExpression pe) {
|
|
||||||
current = pe.getExpression();
|
|
||||||
}
|
|
||||||
while (current instanceof CastExpression ce) {
|
|
||||||
current = ce.getExpression();
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,6 @@ package click.kamil.springstatemachineexporter.analysis.model;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
|
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
|
||||||
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.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -38,19 +31,10 @@ public class AnalysisResult {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private List<BusinessFlow> flows = java.util.Collections.emptyList();
|
private List<BusinessFlow> flows = java.util.Collections.emptyList();
|
||||||
|
|
||||||
/** Resolved {@code StateMachineConfigurerAdapter<S,E>} state type; persisted for JSON round-trip. */
|
|
||||||
private String stateTypeFqn;
|
|
||||||
/** Resolved {@code StateMachineConfigurerAdapter<S,E>} event type; persisted for JSON round-trip. */
|
|
||||||
private String eventTypeFqn;
|
|
||||||
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
||||||
|
|
||||||
public void applyResolution(Map<String, String> properties) {
|
public void applyResolution(Map<String, String> properties) {
|
||||||
applyResolution(properties, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void applyResolution(Map<String, String> properties, CodebaseContext context) {
|
|
||||||
var resolver = new click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver();
|
var resolver = new click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver();
|
||||||
|
|
||||||
// 1. Resolve start/end states strings
|
// 1. Resolve start/end states strings
|
||||||
@@ -68,9 +52,7 @@ public class AnalysisResult {
|
|||||||
// 2. Resolve states (State record is immutable, so recreate)
|
// 2. Resolve states (State record is immutable, so recreate)
|
||||||
if (states != null) {
|
if (states != null) {
|
||||||
this.states = states.stream()
|
this.states = states.stream()
|
||||||
.map(s -> click.kamil.springstatemachineexporter.model.State.of(
|
.map(s -> click.kamil.springstatemachineexporter.model.State.of(s.rawName(), resolver.resolveValue(s.fullIdentifier(), properties)))
|
||||||
resolver.resolveValue(s.rawName(), properties),
|
|
||||||
resolver.resolveValue(s.fullIdentifier(), properties)))
|
|
||||||
.collect(java.util.stream.Collectors.toSet());
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,18 +72,15 @@ public class AnalysisResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Resolve metadata (triggers, entry points, call chains)
|
// 4. Resolve metadata (triggers, entry points)
|
||||||
if (metadata != null) {
|
if (metadata != null) {
|
||||||
List<TriggerPoint> resolvedTriggers = metadata.getTriggers() == null ? null
|
if (metadata.getTriggers() != null) {
|
||||||
: metadata.getTriggers().stream()
|
for (var trigger : metadata.getTriggers()) {
|
||||||
.map(trigger -> resolveTrigger(trigger, resolver, properties, context))
|
if (trigger.getEvent() != null) {
|
||||||
.collect(java.util.stream.Collectors.toList());
|
trigger.setEvent(resolver.resolveValue(trigger.getEvent(), properties));
|
||||||
|
}
|
||||||
List<CallChain> resolvedCallChains = metadata.getCallChains() == null ? null
|
}
|
||||||
: metadata.getCallChains().stream()
|
}
|
||||||
.map(chain -> resolveCallChain(chain, resolver, properties, context))
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
|
|
||||||
if (metadata.getEntryPoints() != null) {
|
if (metadata.getEntryPoints() != null) {
|
||||||
for (var ep : metadata.getEntryPoints()) {
|
for (var ep : metadata.getEntryPoints()) {
|
||||||
if (ep.getName() != null) {
|
if (ep.getName() != null) {
|
||||||
@@ -116,76 +95,7 @@ public class AnalysisResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.metadata = CodebaseMetadata.builder()
|
|
||||||
.triggers(resolvedTriggers != null ? resolvedTriggers : metadata.getTriggers())
|
|
||||||
.entryPoints(metadata.getEntryPoints())
|
|
||||||
.callChains(resolvedCallChains != null ? resolvedCallChains : metadata.getCallChains())
|
|
||||||
.properties(metadata.getProperties())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canonicalizeResolvedIdentifiers();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void canonicalizeResolvedIdentifiers() {
|
|
||||||
if (stateTypeFqn == null && eventTypeFqn == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
click.kamil.springstatemachineexporter.analysis.service.AnalysisResultFinalizer.applyCanonicalization(
|
|
||||||
this,
|
|
||||||
null,
|
|
||||||
new StateMachineTypeResolver.MachineTypes(stateTypeFqn, eventTypeFqn));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TriggerPoint resolveTrigger(
|
|
||||||
TriggerPoint trigger,
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
|
||||||
Map<String, String> properties,
|
|
||||||
CodebaseContext context) {
|
|
||||||
List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
|
|
||||||
: trigger.getPolymorphicEvents().stream()
|
|
||||||
.map(value -> resolver.resolveValue(value, properties))
|
|
||||||
.map(value -> MachineEnumCanonicalizer.qualifyEventIdentifier(
|
|
||||||
value, trigger.getEventTypeFqn(), context))
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
String resolvedEvent = trigger.getEvent() != null ? resolver.resolveValue(trigger.getEvent(), properties) : null;
|
|
||||||
String resolvedSource = trigger.getSourceState() != null
|
|
||||||
? resolver.resolveValue(trigger.getSourceState(), properties)
|
|
||||||
: null;
|
|
||||||
return trigger.toBuilder()
|
|
||||||
.event(resolvedEvent != null
|
|
||||||
? MachineEnumCanonicalizer.qualifyEventIdentifier(
|
|
||||||
resolvedEvent, trigger.getEventTypeFqn(), context)
|
|
||||||
: null)
|
|
||||||
.sourceState(resolvedSource != null
|
|
||||||
? MachineEnumCanonicalizer.canonicalizeLabel(
|
|
||||||
resolvedSource, trigger.getStateTypeFqn(), context)
|
|
||||||
: null)
|
|
||||||
.polymorphicEvents(polymorphicEvents)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CallChain resolveCallChain(
|
|
||||||
CallChain chain,
|
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
|
||||||
Map<String, String> properties,
|
|
||||||
CodebaseContext context) {
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
|
||||||
? null
|
|
||||||
: resolveTrigger(chain.getTriggerPoint(), resolver, properties, context);
|
|
||||||
List<MatchedTransition> matchedTransitions = chain.getMatchedTransitions() == null ? null
|
|
||||||
: chain.getMatchedTransitions().stream()
|
|
||||||
.map(matched -> MatchedTransition.builder()
|
|
||||||
.event(resolver.resolveValue(matched.getEvent(), properties))
|
|
||||||
.sourceState(resolver.resolveValue(matched.getSourceState(), properties))
|
|
||||||
.targetState(resolver.resolveValue(matched.getTargetState(), properties))
|
|
||||||
.build())
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
return chain.toBuilder()
|
|
||||||
.triggerPoint(trigger)
|
|
||||||
.matchedTransitions(matchedTransitions)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addMetadata(CodebaseMetadata newMetadata) {
|
public void addMetadata(CodebaseMetadata newMetadata) {
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import lombok.extern.jackson.Jacksonized;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder(toBuilder = true)
|
@Builder
|
||||||
@Jacksonized
|
@Jacksonized
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class BusinessFlow {
|
public class BusinessFlow {
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String description;
|
private final String description;
|
||||||
private final List<FlowStep> steps;
|
private final List<String> steps; // List of Event names in order
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.extern.jackson.Jacksonized;
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder(toBuilder = true)
|
@Builder(toBuilder = true)
|
||||||
@@ -19,8 +17,4 @@ public class CallChain {
|
|||||||
private final TriggerPoint triggerPoint;
|
private final TriggerPoint triggerPoint;
|
||||||
private final String contextMachineId;
|
private final String contextMachineId;
|
||||||
private final List<MatchedTransition> matchedTransitions;
|
private final List<MatchedTransition> matchedTransitions;
|
||||||
/** How this chain's trigger was linked; derived from trigger flags and matchedTransitions. */
|
|
||||||
private final LinkResolution linkResolution;
|
|
||||||
@JsonIgnore
|
|
||||||
private final Map<String, String> pathBindings;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class CallEdge {
|
public class CallEdge {
|
||||||
private String targetMethod;
|
private final String targetMethod;
|
||||||
private List<String> arguments;
|
private final List<String> arguments;
|
||||||
private String receiver;
|
|
||||||
private String constraint;
|
|
||||||
|
|
||||||
public CallEdge(String targetMethod, List<String> arguments) {
|
|
||||||
this(targetMethod, arguments, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CallEdge(String targetMethod, List<String> arguments, String receiver) {
|
|
||||||
this(targetMethod, arguments, receiver, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder(toBuilder = true)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
@JsonDeserialize(using = FlowStepDeserializer.class)
|
|
||||||
@JsonSerialize(using = FlowStepSerializer.class)
|
|
||||||
public class FlowStep {
|
|
||||||
/** Source state identifier (package-canonical FQN or short form). */
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
private final String source;
|
|
||||||
/** Event identifier or {@code Source->Target} for anonymous transitions. */
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
private final String event;
|
|
||||||
/** Precomputed {@code #link_*} suffix for HTML/SVG highlight. */
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
private final String linkKey;
|
|
||||||
|
|
||||||
public static FlowStep ofEvent(String event) {
|
|
||||||
return FlowStep.builder().event(event).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
|
||||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
class FlowStepDeserializer extends JsonDeserializer<FlowStep> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FlowStep deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
|
||||||
JsonNode node = parser.getCodec().readTree(parser);
|
|
||||||
if (node.isTextual()) {
|
|
||||||
return FlowStep.builder().event(node.asText()).build();
|
|
||||||
}
|
|
||||||
return FlowStep.builder()
|
|
||||||
.source(textOrNull(node, "source"))
|
|
||||||
.event(textOrNull(node, "event"))
|
|
||||||
.linkKey(textOrNull(node, "linkKey"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String textOrNull(JsonNode node, String field) {
|
|
||||||
JsonNode value = node.get(field);
|
|
||||||
if (value == null || value.isNull()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String text = value.asText();
|
|
||||||
return text.isBlank() ? null : text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
class FlowStepSerializer extends JsonSerializer<FlowStep> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void serialize(FlowStep step, JsonGenerator generator, SerializerProvider serializers) throws IOException {
|
|
||||||
if (step == null) {
|
|
||||||
generator.writeNull();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boolean structured = step.getSource() != null && !step.getSource().isBlank()
|
|
||||||
|| step.getLinkKey() != null && !step.getLinkKey().isBlank();
|
|
||||||
if (!structured && step.getEvent() != null && !step.getEvent().isBlank()) {
|
|
||||||
generator.writeString(step.getEvent());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
generator.writeStartObject();
|
|
||||||
if (step.getSource() != null && !step.getSource().isBlank()) {
|
|
||||||
generator.writeStringField("source", step.getSource());
|
|
||||||
}
|
|
||||||
if (step.getEvent() != null && !step.getEvent().isBlank()) {
|
|
||||||
generator.writeStringField("event", step.getEvent());
|
|
||||||
}
|
|
||||||
if (step.getLinkKey() != null && !step.getLinkKey().isBlank()) {
|
|
||||||
generator.writeStringField("linkKey", step.getLinkKey());
|
|
||||||
}
|
|
||||||
generator.writeEndObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthetic trigger markers for persistence/lifecycle operations that do not fire
|
|
||||||
* {@code sendEvent} transitions (e.g. {@code StateMachinePersister.restore(...)}).
|
|
||||||
*/
|
|
||||||
public final class LifecycleTriggerMarkers {
|
|
||||||
|
|
||||||
public static final String PREFIX = "[LIFECYCLE:";
|
|
||||||
|
|
||||||
private LifecycleTriggerMarkers() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isLifecycle(String event) {
|
|
||||||
return event != null && event.startsWith(PREFIX) && event.endsWith("]");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String lifecycleType(String event) {
|
|
||||||
if (!isLifecycle(event)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return event.substring(PREFIX.length(), event.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String displayLabel(String event) {
|
|
||||||
String type = lifecycleType(event);
|
|
||||||
if (type == null) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
return switch (type.toUpperCase()) {
|
|
||||||
case "RESTORE" -> "Restore persisted state";
|
|
||||||
case "RESET" -> "Reset state machine";
|
|
||||||
default -> "Lifecycle: " + type.toLowerCase();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export metadata describing how a call-chain trigger was linked to state-machine transitions.
|
|
||||||
* Derived from existing trigger flags only — no additional heuristics.
|
|
||||||
*/
|
|
||||||
public enum LinkResolution {
|
|
||||||
/** Concrete trigger linked to one or more matched transitions. */
|
|
||||||
RESOLVED,
|
|
||||||
/** REST or other external trigger that cannot be statically linked. */
|
|
||||||
UNRESOLVED_EXTERNAL,
|
|
||||||
/** Call-graph widening or source-state ambiguity prevented linking. */
|
|
||||||
AMBIGUOUS_WIDEN,
|
|
||||||
/** Static trigger with no matching transition link. */
|
|
||||||
NO_MATCH
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.extern.jackson.Jacksonized;
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder(toBuilder = true)
|
@Builder
|
||||||
@Jacksonized
|
@Jacksonized
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class MatchedTransition {
|
public class MatchedTransition {
|
||||||
private final String sourceState;
|
private final String sourceState;
|
||||||
private final String targetState;
|
private final String targetState;
|
||||||
private final String event;
|
private final String event;
|
||||||
/** Precomputed {@code #link_*} suffix for HTML/SVG highlight; optional in JSON exports. */
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
private final String linkKey;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.model;
|
package click.kamil.springstatemachineexporter.analysis.model;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -9,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder(toBuilder = true)
|
@Builder
|
||||||
@Jacksonized
|
@Jacksonized
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class TriggerPoint {
|
public class TriggerPoint {
|
||||||
@@ -21,43 +20,5 @@ public class TriggerPoint {
|
|||||||
private final String stateMachineId; // Optional: to link to a specific SM instance
|
private final String stateMachineId; // Optional: to link to a specific SM instance
|
||||||
private final String sourceState; // Optional: if we can determine the expected current state
|
private final String sourceState; // Optional: if we can determine the expected current state
|
||||||
private final int lineNumber;
|
private final int lineNumber;
|
||||||
@com.fasterxml.jackson.annotation.JsonIgnore
|
|
||||||
private final String stateTypeFqn; // Type of State (e.g. OrderStates FQN)
|
|
||||||
@com.fasterxml.jackson.annotation.JsonIgnore
|
|
||||||
private final String eventTypeFqn; // Type of Event (e.g. OrderEvents FQN)
|
|
||||||
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
|
private final java.util.List<String> polymorphicEvents; // NEW: stores concrete events resolved via deep polymorphism
|
||||||
private final boolean external;
|
|
||||||
private final String constraint;
|
|
||||||
private final boolean ambiguous;
|
|
||||||
|
|
||||||
public TriggerPoint(
|
|
||||||
String event,
|
|
||||||
String className,
|
|
||||||
String methodName,
|
|
||||||
String sourceFile,
|
|
||||||
String sourceModule,
|
|
||||||
String stateMachineId,
|
|
||||||
String sourceState,
|
|
||||||
int lineNumber,
|
|
||||||
String stateTypeFqn,
|
|
||||||
String eventTypeFqn,
|
|
||||||
java.util.List<String> polymorphicEvents,
|
|
||||||
boolean external,
|
|
||||||
String constraint,
|
|
||||||
boolean ambiguous) {
|
|
||||||
this.className = className;
|
|
||||||
this.methodName = methodName;
|
|
||||||
this.sourceFile = sourceFile;
|
|
||||||
this.sourceModule = sourceModule;
|
|
||||||
this.stateMachineId = stateMachineId;
|
|
||||||
this.sourceState = sourceState;
|
|
||||||
this.lineNumber = lineNumber;
|
|
||||||
this.stateTypeFqn = stateTypeFqn;
|
|
||||||
this.eventTypeFqn = eventTypeFqn;
|
|
||||||
this.event = event;
|
|
||||||
this.polymorphicEvents = polymorphicEvents;
|
|
||||||
this.external = external;
|
|
||||||
this.constraint = constraint;
|
|
||||||
this.ambiguous = ambiguous;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,485 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorKind;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.GetterChainEdge;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.GetterBodyConstantScanner;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified accessor resolution pipeline: index lookup first, then field/CIC/method-body fallbacks.
|
|
||||||
*/
|
|
||||||
public final class AccessorResolver {
|
|
||||||
|
|
||||||
public record GetterChainStep(String typeFqn, ClassInstanceCreation cic) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ReceiverContext(
|
|
||||||
TypeDeclaration ownerType,
|
|
||||||
ClassInstanceCreation cic,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
boolean anonymousGetter,
|
|
||||||
Map<String, String> fieldValues) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private final CodebaseContext context;
|
|
||||||
private final ConstantExtractor constantExtractor;
|
|
||||||
private final ConstructorAnalyzer constructorAnalyzer;
|
|
||||||
private final ConstantResolver constantResolver;
|
|
||||||
private final FieldInitializerFinder fieldInitializerFinder;
|
|
||||||
private final Map<String, List<String>> sessionCache = new HashMap<>();
|
|
||||||
private final Map<String, Optional<AccessorSummary>> indexedGetterCache = new HashMap<>();
|
|
||||||
|
|
||||||
public AccessorResolver(
|
|
||||||
CodebaseContext context,
|
|
||||||
ConstantExtractor constantExtractor,
|
|
||||||
ConstructorAnalyzer constructorAnalyzer,
|
|
||||||
ConstantResolver constantResolver) {
|
|
||||||
this.context = context;
|
|
||||||
this.constantExtractor = constantExtractor;
|
|
||||||
this.constructorAnalyzer = constructorAnalyzer;
|
|
||||||
this.constantResolver = constantResolver;
|
|
||||||
this.fieldInitializerFinder = new FieldInitializerFinder(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName) {
|
|
||||||
if (className == null || className.isBlank()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
String cacheKey = className + "#" + methodName;
|
|
||||||
if (indexedGetterCache.containsKey(cacheKey)) {
|
|
||||||
return indexedGetterCache.get(cacheKey);
|
|
||||||
}
|
|
||||||
Optional<AccessorSummary> resolved = lookupIndexedGetterInternal(className, methodName);
|
|
||||||
indexedGetterCache.put(cacheKey, resolved);
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> lookupIndexedGetterInternal(String className, String methodName) {
|
|
||||||
Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
|
|
||||||
if (direct.isPresent()) {
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
|
||||||
if (typeDeclaration != null) {
|
|
||||||
direct = context.getAccessorIndex().lookup(context.getFqn(typeDeclaration), methodName);
|
|
||||||
if (direct.isPresent()) {
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
if (!typeDeclaration.isInterface() && !org.eclipse.jdt.core.dom.Modifier.isAbstract(typeDeclaration.getModifiers())) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<String> implementations = context.getImplementations(className);
|
|
||||||
if (implementations == null || implementations.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Optional<AccessorSummary> precomputed = context.getPolymorphicAccessorIndex()
|
|
||||||
.representativeImplAccessor(className, methodName);
|
|
||||||
if (precomputed.isPresent()) {
|
|
||||||
return precomputed;
|
|
||||||
}
|
|
||||||
for (String implementation : implementations) {
|
|
||||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
|
||||||
if (implAccessor.isPresent()) {
|
|
||||||
return implAccessor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> resolve(String ownerFqn, String methodName, ReceiverContext receiverContext, ResolutionBudget budget) {
|
|
||||||
return resolve(ownerFqn, methodName, receiverContext, budget, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> resolve(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
ReceiverContext receiverContext,
|
|
||||||
ResolutionBudget budget,
|
|
||||||
Set<String> visited) {
|
|
||||||
if (ownerFqn == null || methodName == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
if (budget == null) {
|
|
||||||
budget = ResolutionBudget.defaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
String cacheKey = cacheKey(ownerFqn, methodName, receiverContext);
|
|
||||||
List<String> cached = sessionCache.get(cacheKey);
|
|
||||||
if (cached != null) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> resolved = resolveInternal(ownerFqn, methodName, receiverContext, budget, visited);
|
|
||||||
if (!resolved.isEmpty() || isStaticCacheable(receiverContext)) {
|
|
||||||
List<String> cachedCopy = List.copyOf(resolved);
|
|
||||||
sessionCache.put(cacheKey, cachedCopy);
|
|
||||||
return cachedCopy;
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetterChainStep resolveGetterChainStep(String ownerFqn, String getterName, ResolutionBudget budget) {
|
|
||||||
if (ownerFqn == null || getterName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<GetterChainEdge> precomputed = context.getGetterChainIndex().lookup(ownerFqn, getterName);
|
|
||||||
if (precomputed.isPresent()) {
|
|
||||||
GetterChainEdge edge = precomputed.get();
|
|
||||||
return new GetterChainStep(edge.nextTypeFqn(), edge.fieldInitializerCic());
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, getterName);
|
|
||||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
|
||||||
AccessorSummary accessor = indexedAccessor.get();
|
|
||||||
ClassInstanceCreation nextCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, accessor.fieldName());
|
|
||||||
String nextType = fieldInitializerFinder.resolveFieldTypeName(ownerFqn, accessor.fieldName());
|
|
||||||
if (nextCic != null) {
|
|
||||||
nextType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType());
|
|
||||||
}
|
|
||||||
if (nextType != null) {
|
|
||||||
return new GetterChainStep(nextType, nextCic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String simpleType = simplifyTypeName(ownerFqn);
|
|
||||||
ClassInstanceCreation nextCic = constructorAnalyzer.findReturnedCIC(simpleType, getterName, context);
|
|
||||||
if (nextCic == null) {
|
|
||||||
String fieldName = indexedFieldName(ownerFqn, getterName);
|
|
||||||
nextCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, fieldName);
|
|
||||||
}
|
|
||||||
if (nextCic != null) {
|
|
||||||
return new GetterChainStep(
|
|
||||||
click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType()),
|
|
||||||
nextCic);
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(simpleType);
|
|
||||||
if (td == null) {
|
|
||||||
td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
}
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
|
|
||||||
if (getter != null && getter.getReturnType2() != null) {
|
|
||||||
return new GetterChainStep(getter.getReturnType2().toString(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isStaticCacheable(ReceiverContext receiverContext) {
|
|
||||||
if (receiverContext == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (receiverContext.cic() != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Map<String, String> fieldValues = receiverContext.fieldValues();
|
|
||||||
return fieldValues == null || fieldValues.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearSessionCache() {
|
|
||||||
sessionCache.clear();
|
|
||||||
indexedGetterCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveInternal(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
ReceiverContext receiverContext,
|
|
||||||
ResolutionBudget budget,
|
|
||||||
Set<String> visited) {
|
|
||||||
TypeDeclaration ownerType = receiverContext != null ? receiverContext.ownerType() : null;
|
|
||||||
if (ownerType == null) {
|
|
||||||
ownerType = context.getTypeDeclaration(ownerFqn);
|
|
||||||
}
|
|
||||||
boolean widenToImplementations = ownerType == null
|
|
||||||
|| ownerType.isInterface()
|
|
||||||
|| org.eclipse.jdt.core.dom.Modifier.isAbstract(ownerType.getModifiers());
|
|
||||||
|
|
||||||
if (widenToImplementations) {
|
|
||||||
List<String> results = new ArrayList<>();
|
|
||||||
|
|
||||||
if (receiverContext != null && receiverContext.cic() != null) {
|
|
||||||
String cicOwnerFqn = resolveCicOwnerFqn(receiverContext);
|
|
||||||
if (cicOwnerFqn != null) {
|
|
||||||
TypeDeclaration cicOwnerType = context.getTypeDeclaration(
|
|
||||||
cicOwnerFqn, receiverContext.contextCu());
|
|
||||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(cicOwnerFqn, methodName);
|
|
||||||
ReceiverContext cicReceiverContext = new ReceiverContext(
|
|
||||||
cicOwnerType,
|
|
||||||
receiverContext.cic(),
|
|
||||||
receiverContext.contextCu(),
|
|
||||||
receiverContext.anonymousGetter(),
|
|
||||||
receiverContext.fieldValues());
|
|
||||||
List<String> cicValues = resolveWithCic(
|
|
||||||
cicOwnerFqn,
|
|
||||||
methodName,
|
|
||||||
cicReceiverContext,
|
|
||||||
indexedAccessor.orElse(null),
|
|
||||||
visited);
|
|
||||||
if (!cicValues.isEmpty()) {
|
|
||||||
return deduplicatePreservingOrder(cicValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
|
|
||||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
|
||||||
results.addAll(constantExtractor.resolveMethodReturnConstant(
|
|
||||||
ownerFqn, methodName, 0, visited, contextCu, budget));
|
|
||||||
}
|
|
||||||
if (!results.isEmpty()) {
|
|
||||||
return deduplicatePreservingOrder(results);
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, methodName);
|
|
||||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
|
||||||
List<String> indexed = resolveIndexedAccessor(indexedAccessor.get(), receiverContext, visited);
|
|
||||||
if (!indexed.isEmpty()) {
|
|
||||||
return indexed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiverContext != null && receiverContext.ownerType() != null && receiverContext.cic() != null) {
|
|
||||||
List<String> cicValues = resolveWithCic(
|
|
||||||
ownerFqn, methodName, receiverContext, indexedAccessor.orElse(null), visited);
|
|
||||||
if (!cicValues.isEmpty()) {
|
|
||||||
return cicValues;
|
|
||||||
}
|
|
||||||
} else if (receiverContext != null && receiverContext.cic() != null) {
|
|
||||||
ReceiverContext enriched = new ReceiverContext(
|
|
||||||
context.getTypeDeclaration(ownerFqn),
|
|
||||||
receiverContext.cic(),
|
|
||||||
receiverContext.contextCu(),
|
|
||||||
receiverContext.anonymousGetter(),
|
|
||||||
receiverContext.fieldValues());
|
|
||||||
List<String> cicValues = resolveWithCic(
|
|
||||||
ownerFqn, methodName, enriched, indexedAccessor.orElse(null), visited);
|
|
||||||
if (!cicValues.isEmpty()) {
|
|
||||||
return cicValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
|
|
||||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
|
||||||
List<String> deep = constantExtractor.resolveMethodReturnConstant(
|
|
||||||
ownerFqn, methodName, 0, visited, contextCu, budget);
|
|
||||||
if (!deep.isEmpty()) {
|
|
||||||
return deep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveIndexedAccessor(
|
|
||||||
AccessorSummary accessor,
|
|
||||||
ReceiverContext receiverContext,
|
|
||||||
Set<String> visited) {
|
|
||||||
if (accessor.kind() == AccessorKind.CONSTANT_GETTER) {
|
|
||||||
List<String> constants = extractConstantGetterReturn(accessor.ownerFqn(), accessor.methodName());
|
|
||||||
if (!constants.isEmpty()) {
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
if (constantExtractor != null) {
|
|
||||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
|
||||||
return constantExtractor.resolveMethodReturnConstant(
|
|
||||||
accessor.ownerFqn(), accessor.methodName(), 0, new HashSet<>(visited), contextCu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (constantExtractor != null) {
|
|
||||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
|
||||||
String requestingTypeFqn = receiverContext != null && receiverContext.ownerType() != null
|
|
||||||
? context.getFqn(receiverContext.ownerType())
|
|
||||||
: accessor.ownerFqn();
|
|
||||||
List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
|
||||||
accessor, requestingTypeFqn, contextCu, visited);
|
|
||||||
if (!fieldConstants.isEmpty()) {
|
|
||||||
return fieldConstants;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receiverContext != null
|
|
||||||
&& receiverContext.fieldValues() != null
|
|
||||||
&& accessor.fieldName() != null) {
|
|
||||||
String fieldValue = receiverContext.fieldValues().get(accessor.fieldName());
|
|
||||||
if (fieldValue != null) {
|
|
||||||
return List.of(fieldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveWithCic(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
ReceiverContext receiverContext,
|
|
||||||
AccessorSummary indexedAccessor,
|
|
||||||
Set<String> visited) {
|
|
||||||
TypeDeclaration td = receiverContext.ownerType();
|
|
||||||
if (td == null) {
|
|
||||||
td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
}
|
|
||||||
ClassInstanceCreation cic = receiverContext.cic();
|
|
||||||
Map<String, String> fieldValues = receiverContext.fieldValues() != null
|
|
||||||
? receiverContext.fieldValues()
|
|
||||||
: new HashMap<>();
|
|
||||||
|
|
||||||
MethodDeclaration getter = null;
|
|
||||||
if (cic.getAnonymousClassDeclaration() != null) {
|
|
||||||
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
|
||||||
if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(methodName)) {
|
|
||||||
getter = md;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (getter == null) {
|
|
||||||
getter = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
}
|
|
||||||
if (getter != null && getter.getBody() != null) {
|
|
||||||
if (fieldValues.isEmpty() && constructorAnalyzer != null) {
|
|
||||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, null));
|
|
||||||
}
|
|
||||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
|
||||||
if (result != null) {
|
|
||||||
return List.of(qualifyEnumLikeResult(getter, result));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (indexedAccessor != null && indexedAccessor.isGetter() && !receiverContext.anonymousGetter()) {
|
|
||||||
if (indexedAccessor.kind() == AccessorKind.CONSTANT_GETTER) {
|
|
||||||
List<String> constantValues = extractConstantGetterReturn(ownerFqn, methodName);
|
|
||||||
if (!constantValues.isEmpty()) {
|
|
||||||
return constantValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (fieldValues.isEmpty() && constructorAnalyzer != null) {
|
|
||||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, null));
|
|
||||||
}
|
|
||||||
if (indexedAccessor.fieldName() != null) {
|
|
||||||
String indexedFieldValue = fieldValues.get(indexedAccessor.fieldName());
|
|
||||||
if (indexedFieldValue != null) {
|
|
||||||
return List.of(indexedFieldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (constantExtractor != null) {
|
|
||||||
List<String> indexedConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
|
||||||
indexedAccessor, ownerFqn, receiverContext.contextCu(), visited);
|
|
||||||
if (!indexedConstants.isEmpty()) {
|
|
||||||
return indexedConstants;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> extractConstantGetterReturn(String ownerFqn, String methodName) {
|
|
||||||
return GetterBodyConstantScanner.extractConstantGetterReturn(
|
|
||||||
context, constantExtractor, constantResolver, ownerFqn, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String qualifyEnumLikeResult(MethodDeclaration getter, String result) {
|
|
||||||
if (result.contains(".")) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
|
|
||||||
if (returnType == null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
int lastDotRet = returnType.lastIndexOf('.');
|
|
||||||
String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
|
|
||||||
if ("String".equals(simpleReturnType) || "Integer".equals(simpleReturnType)
|
|
||||||
|| "Long".equals(simpleReturnType) || "Boolean".equals(simpleReturnType)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return simpleReturnType + "." + result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String indexedFieldName(String ownerFqn, String getterName) {
|
|
||||||
Optional<AccessorSummary> accessorOpt = lookupIndexedGetter(ownerFqn, getterName);
|
|
||||||
if (accessorOpt.isEmpty() || accessorOpt.get().fieldName() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return accessorOpt.get().fieldName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveCicOwnerFqn(ReceiverContext receiverContext) {
|
|
||||||
if (receiverContext == null || receiverContext.cic() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
ClassInstanceCreation cic = receiverContext.cic();
|
|
||||||
CompilationUnit contextCu = receiverContext.contextCu();
|
|
||||||
String simpleName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
|
||||||
TypeDeclaration cicType = contextCu != null
|
|
||||||
? context.getTypeDeclaration(simpleName, contextCu)
|
|
||||||
: null;
|
|
||||||
if (cicType == null) {
|
|
||||||
cicType = context.getTypeDeclaration(simpleName);
|
|
||||||
}
|
|
||||||
if (cicType != null) {
|
|
||||||
return context.getFqn(cicType);
|
|
||||||
}
|
|
||||||
if (cic.resolveTypeBinding() != null) {
|
|
||||||
return cic.resolveTypeBinding().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
return simpleName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> deduplicatePreservingOrder(List<String> values) {
|
|
||||||
List<String> deduped = new ArrayList<>();
|
|
||||||
for (String value : values) {
|
|
||||||
if (!deduped.contains(value)) {
|
|
||||||
deduped.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deduped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String cacheKey(String ownerFqn, String methodName, ReceiverContext receiverContext) {
|
|
||||||
String cicKey = receiverContext != null && receiverContext.cic() != null
|
|
||||||
? receiverContext.cic().toString()
|
|
||||||
: "";
|
|
||||||
String fieldValuesKey = receiverContext != null && receiverContext.fieldValues() != null
|
|
||||||
? receiverContext.fieldValues().toString()
|
|
||||||
: "";
|
|
||||||
return ownerFqn + "#" + methodName + "#" + cicKey + "#" + fieldValuesKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String simplifyTypeName(String typeFqn) {
|
|
||||||
if (typeFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
|
|
||||||
if (simple.contains("<")) {
|
|
||||||
simple = simple.substring(0, simple.indexOf('<'));
|
|
||||||
}
|
|
||||||
return simple;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import java.util.AbstractMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Memoizes polymorphic class compatibility checks used during call-graph path matching.
|
|
||||||
*/
|
|
||||||
public final class ClassCompatibilityCache {
|
|
||||||
|
|
||||||
private final Map<Map.Entry<String, String>, Boolean> cache = new ConcurrentHashMap<>();
|
|
||||||
private final Map<String, Object> memoCache = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public boolean areClassesCompatible(String classNeighbor, String classTarget, ClassCompatibilityChecker checker) {
|
|
||||||
if (classNeighbor == null || classTarget == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (classNeighbor.equals(classTarget)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Map.Entry<String, String> key = cacheKey(classNeighbor, classTarget);
|
|
||||||
return cache.computeIfAbsent(key, ignored -> checker.check(classNeighbor, classTarget));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <T> T memoize(String key, java.util.function.Function<String, T> computer) {
|
|
||||||
return (T) memoCache.computeIfAbsent(key, computer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearMemoCache() {
|
|
||||||
memoCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
cache.clear();
|
|
||||||
memoCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map.Entry<String, String> cacheKey(String classNeighbor, String classTarget) {
|
|
||||||
if (classNeighbor.compareTo(classTarget) <= 0) {
|
|
||||||
return new AbstractMap.SimpleImmutableEntry<>(classNeighbor, classTarget);
|
|
||||||
}
|
|
||||||
return new AbstractMap.SimpleImmutableEntry<>(classTarget, classNeighbor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FunctionalInterface
|
|
||||||
public interface ClassCompatibilityChecker {
|
|
||||||
boolean check(String classNeighbor, String classTarget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds {@code new ...()} field initializers, walking the inheritance chain once.
|
|
||||||
*/
|
|
||||||
public final class FieldInitializerFinder {
|
|
||||||
|
|
||||||
private final CodebaseContext context;
|
|
||||||
|
|
||||||
public FieldInitializerFinder(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClassInstanceCreation findFieldInitializerCic(String ownerFqn, String fieldName) {
|
|
||||||
if (fieldName == null || ownerFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
return findFieldInitializerCic(td, fieldName, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClassInstanceCreation findFieldInitializerCic(TypeDeclaration td, String fieldName, Set<String> visited) {
|
|
||||||
if (td == null || fieldName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String typeFqn = context.getFqn(td);
|
|
||||||
if (typeFqn == null || !visited.add(typeFqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
if (fragObj instanceof VariableDeclarationFragment frag
|
|
||||||
&& frag.getName().getIdentifier().equals(fieldName)
|
|
||||||
&& frag.getInitializer() instanceof ClassInstanceCreation cic) {
|
|
||||||
return cic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
|
||||||
if (superFqn != null) {
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
ClassInstanceCreation inherited = findFieldInitializerCic(superTd, fieldName, visited);
|
|
||||||
if (inherited != null) {
|
|
||||||
return inherited;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String resolveFieldTypeName(String ownerFqn, String fieldName) {
|
|
||||||
if (ownerFqn == null || fieldName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
|
|
||||||
return resolveFieldTypeName(td, fieldName, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String resolveFieldTypeName(TypeDeclaration td, String fieldName, Set<String> visited) {
|
|
||||||
if (td == null || fieldName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String typeFqn = context.getFqn(td);
|
|
||||||
if (typeFqn == null || !visited.add(typeFqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FieldDeclaration fd : td.getFields()) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
if (fragObj instanceof VariableDeclarationFragment frag
|
|
||||||
&& frag.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
return fd.getType().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
|
||||||
if (superFqn != null) {
|
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
|
||||||
return resolveFieldTypeName(superTd, fieldName, visited);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scans trivial getter bodies for constant return values.
|
|
||||||
*/
|
|
||||||
public final class GetterBodyConstantScanner {
|
|
||||||
|
|
||||||
private GetterBodyConstantScanner() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> extractConstantGetterReturn(
|
|
||||||
CodebaseContext context,
|
|
||||||
ConstantExtractor constantExtractor,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName) {
|
|
||||||
return extractConstantGetterReturn(context, constantExtractor, constantResolver, ownerFqn, methodName, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> extractConstantGetterReturn(
|
|
||||||
CodebaseContext context,
|
|
||||||
ConstantExtractor constantExtractor,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
Set<String> ignoredVisited) {
|
|
||||||
TypeDeclaration typeDeclaration = contextCu != null
|
|
||||||
? context.getTypeDeclaration(ownerFqn, contextCu)
|
|
||||||
: null;
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
|
||||||
}
|
|
||||||
if (typeDeclaration == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
|
||||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> extractor =
|
|
||||||
constantExtractor != null
|
|
||||||
? constantExtractor::extractConstantsFromExpression
|
|
||||||
: (expr, out) -> {
|
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
|
||||||
if (resolved != null) {
|
|
||||||
out.add(resolved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (node.getExpression() != null) {
|
|
||||||
extractor.accept(node.getExpression(), constants);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Central registry for reactive/factory unwrap and passthrough method names used during constant extraction.
|
|
||||||
*/
|
|
||||||
public final class LibraryUnwrapRegistry {
|
|
||||||
|
|
||||||
private static final Set<String> UNWRAP_RECEIVER_TYPES = Set.of(
|
|
||||||
"Optional", "Stream", "List", "Set", "Arrays", "Objects", "Mono", "Flux");
|
|
||||||
|
|
||||||
private static final Set<String> UNWRAP_METHOD_NAMES = Set.of(
|
|
||||||
"of", "ofEntries", "asList", "entry", "just", "withPayload", "success");
|
|
||||||
|
|
||||||
private static final Set<String> REACTIVE_FACTORY_METHODS = Set.of("just", "withPayload", "success");
|
|
||||||
|
|
||||||
private static final Set<String> REACTIVE_TRANSFORM_METHODS = Set.of("map", "flatMap", "switchMap", "concatMap");
|
|
||||||
|
|
||||||
private static final String[] STOP_UNWRAP_PREFIXES = {
|
|
||||||
"map", "parse", "convert", "to", "resolve", "build", "create", "from",
|
|
||||||
"transform", "translate", "derive", "determine", "calculate", "decode",
|
|
||||||
"extract", "get", "find", "validate"
|
|
||||||
};
|
|
||||||
|
|
||||||
private LibraryUnwrapRegistry() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isUnwrapReceiverType(String receiverExpressionText) {
|
|
||||||
return receiverExpressionText != null && UNWRAP_RECEIVER_TYPES.contains(receiverExpressionText);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isUnwrapMethodName(String methodName) {
|
|
||||||
return methodName != null && UNWRAP_METHOD_NAMES.contains(methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isReactiveFactoryMethod(String methodName) {
|
|
||||||
return methodName != null && REACTIVE_FACTORY_METHODS.contains(methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isReactiveTransformMethod(String methodName) {
|
|
||||||
return methodName != null && REACTIVE_TRANSFORM_METHODS.contains(methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldStopUnwrap(String methodName) {
|
|
||||||
if (methodName == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String lowerName = methodName.toLowerCase(Locale.ROOT);
|
|
||||||
for (String prefix : STOP_UNWRAP_PREFIXES) {
|
|
||||||
if (lowerName.startsWith(prefix)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isEventSetterMethodName(String methodName) {
|
|
||||||
if (methodName == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return "event".equalsIgnoreCase(methodName)
|
|
||||||
|| "type".equalsIgnoreCase(methodName)
|
|
||||||
|| "eventType".equalsIgnoreCase(methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isHeaderSetterMethodName(String methodName) {
|
|
||||||
return "setHeader".equals(methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared {@link MethodInvocation} peeling for factory/reactive wrappers and local passthrough methods.
|
|
||||||
*/
|
|
||||||
public final class MethodInvocationUnwrapper {
|
|
||||||
|
|
||||||
private MethodInvocationUnwrapper() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Expression unwrap(
|
|
||||||
MethodInvocation mi,
|
|
||||||
int depth,
|
|
||||||
String methodFqn,
|
|
||||||
CodebaseContext context,
|
|
||||||
Function<MethodInvocation, String> targetMethodFqnResolver) {
|
|
||||||
if (ResolutionBudget.defaults().isUnwrapExhausted(depth)) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
String targetMethodFqn = targetMethodFqnResolver != null
|
|
||||||
? targetMethodFqnResolver.apply(mi)
|
|
||||||
: null;
|
|
||||||
if (targetMethodFqn == null && methodFqn != null && methodFqn.contains(".") && mi.getExpression() == null) {
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
targetMethodFqn = className + "." + mi.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetMethodFqn != null && targetMethodFqn.contains(".")) {
|
|
||||||
String className = targetMethodFqn.substring(0, targetMethodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = targetMethodFqn.substring(targetMethodFqn.lastIndexOf('.') + 1);
|
|
||||||
|
|
||||||
TypeDeclaration currentTd = methodFqn != null && methodFqn.contains(".")
|
|
||||||
? context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')))
|
|
||||||
: null;
|
|
||||||
org.eclipse.jdt.core.dom.CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof org.eclipse.jdt.core.dom.CompilationUnit compilationUnit
|
|
||||||
? compilationUnit
|
|
||||||
: null;
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className, cu);
|
|
||||||
if (td == null) {
|
|
||||||
td = context.getTypeDeclaration(className);
|
|
||||||
}
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration localMd = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
if (localMd != null) {
|
|
||||||
int paramIdx = getReturnedParameterIndex(localMd);
|
|
||||||
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
|
||||||
Expression arg = peelExpression((Expression) mi.arguments().get(paramIdx));
|
|
||||||
if (arg instanceof MethodInvocation innerMi) {
|
|
||||||
return unwrap(innerMi, depth + 1, methodFqn, context, targetMethodFqnResolver);
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mi.getExpression() != null) {
|
|
||||||
String exprStr = mi.getExpression().toString();
|
|
||||||
if (!LibraryUnwrapRegistry.isUnwrapReceiverType(exprStr)) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
} else if (targetMethodFqn == null) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mi.arguments().isEmpty()) {
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
if (LibraryUnwrapRegistry.shouldStopUnwrap(methodName)) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression arg = peelExpression((Expression) mi.arguments().get(0));
|
|
||||||
if (arg instanceof MethodInvocation innerMi) {
|
|
||||||
return unwrap(innerMi, depth + 1, methodFqn, context, targetMethodFqnResolver);
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
}
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Expression peelExpression(Expression expression) {
|
|
||||||
Expression current = expression;
|
|
||||||
while (current instanceof ParenthesizedExpression pe) {
|
|
||||||
current = pe.getExpression();
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static 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
|
|
||||||
&& svd.getName().getIdentifier().equals(retStr)) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.Block;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.LambdaExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts terminal payload literals from reactive factory chains ({@code Mono.just}, nested transforms).
|
|
||||||
*/
|
|
||||||
public final class ReactiveExpressionSupport {
|
|
||||||
|
|
||||||
private ReactiveExpressionSupport() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String extractPayload(Expression expression, ConstantResolver constantResolver, CodebaseContext context) {
|
|
||||||
return extractPayload(expression, null, constantResolver, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Peels {@code just}/{@code withPayload}/{@code success} wrappers on an invocation chain and returns
|
|
||||||
* the payload {@link Expression} for dataflow analysis.
|
|
||||||
*/
|
|
||||||
public static Expression peelFactoryPayloadExpression(Expression expression) {
|
|
||||||
if (!(expression instanceof MethodInvocation mi)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodInvocation current = mi;
|
|
||||||
while (current != null) {
|
|
||||||
String methodName = current.getName().getIdentifier();
|
|
||||||
if (LibraryUnwrapRegistry.isReactiveFactoryMethod(methodName) && !current.arguments().isEmpty()) {
|
|
||||||
return MethodInvocationUnwrapper.peelExpression((Expression) current.arguments().get(0));
|
|
||||||
}
|
|
||||||
Expression receiver = current.getExpression();
|
|
||||||
if (receiver instanceof MethodInvocation nextMi) {
|
|
||||||
current = nextMi;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Expression lambdaBodyExpression(LambdaExpression lambda) {
|
|
||||||
if (lambda.getBody() instanceof Expression bodyExpression) {
|
|
||||||
return bodyExpression;
|
|
||||||
}
|
|
||||||
if (lambda.getBody() instanceof Block block) {
|
|
||||||
for (Object statement : block.statements()) {
|
|
||||||
if (statement instanceof ReturnStatement returnStatement
|
|
||||||
&& returnStatement.getExpression() != null) {
|
|
||||||
return returnStatement.getExpression();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rewrites {@code p.getEvent()} inside a reactive transform lambda to {@code payload.getEvent()}
|
|
||||||
* when {@code p} is fed by {@code Mono.just(payload)} (or a chained reactive source).
|
|
||||||
*/
|
|
||||||
public static String remapLambdaParameterGetter(
|
|
||||||
Expression eventExpr,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (!(eventExpr instanceof MethodInvocation getterCall)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!(getterCall.getExpression() instanceof SimpleName paramName)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
LambdaExpression lambda = findEnclosingLambda(getterCall);
|
|
||||||
if (lambda == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodInvocation transformCall = findEnclosingReactiveTransform(lambda);
|
|
||||||
if (transformCall == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String mappedReceiver = mapLambdaParameterToSource(
|
|
||||||
lambda,
|
|
||||||
paramName.getIdentifier(),
|
|
||||||
transformCall.getExpression(),
|
|
||||||
constantResolver,
|
|
||||||
context);
|
|
||||||
if (mappedReceiver == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return mappedReceiver + "." + getterCall.getName().getIdentifier() + "()";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as {@link #remapLambdaParameterGetter(Expression, ConstantResolver, CodebaseContext)} but locates
|
|
||||||
* the getter in a real method body when {@code expressionText} came from a detached parse tree.
|
|
||||||
*/
|
|
||||||
public static String remapLambdaParameterGetterInMethod(
|
|
||||||
String expressionText,
|
|
||||||
String methodFqn,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
CodebaseContext context) {
|
|
||||||
MethodInvocation getter = AstUtils.findMethodInvocationInMethod(methodFqn, expressionText, context);
|
|
||||||
if (getter == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String remapped = remapLambdaParameterGetter(getter, constantResolver, context);
|
|
||||||
if (remapped != null) {
|
|
||||||
return remapped;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LambdaExpression findEnclosingLambda(ASTNode node) {
|
|
||||||
ASTNode current = node.getParent();
|
|
||||||
while (current != null) {
|
|
||||||
if (current instanceof LambdaExpression lambda) {
|
|
||||||
return lambda;
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MethodInvocation findEnclosingReactiveTransform(ASTNode node) {
|
|
||||||
ASTNode current = node.getParent();
|
|
||||||
while (current != null) {
|
|
||||||
if (current instanceof MethodInvocation mi
|
|
||||||
&& LibraryUnwrapRegistry.isReactiveTransformMethod(mi.getName().getIdentifier())) {
|
|
||||||
return mi;
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractPayload(
|
|
||||||
Expression expression,
|
|
||||||
Expression flatMapReceiver,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (expression == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (expression instanceof MethodInvocation mi) {
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
if (LibraryUnwrapRegistry.isReactiveTransformMethod(methodName) && !mi.arguments().isEmpty()) {
|
|
||||||
String fromArgument = extractTransformArgumentPayload(
|
|
||||||
(Expression) mi.arguments().get(0), mi.getExpression(), constantResolver, context);
|
|
||||||
if (fromArgument != null) {
|
|
||||||
return fromArgument;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (LibraryUnwrapRegistry.isReactiveFactoryMethod(methodName) && !mi.arguments().isEmpty()) {
|
|
||||||
Expression arg = (Expression) mi.arguments().get(0);
|
|
||||||
if (constantResolver != null && context != null) {
|
|
||||||
String resolved = constantResolver.resolve(arg, context);
|
|
||||||
if (resolved != null) {
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arg.toString();
|
|
||||||
}
|
|
||||||
if (mi.getExpression() != null) {
|
|
||||||
return extractPayload(mi.getExpression(), flatMapReceiver, constantResolver, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractTransformArgumentPayload(
|
|
||||||
Expression argument,
|
|
||||||
Expression transformReceiver,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (argument instanceof LambdaExpression lambda) {
|
|
||||||
Expression bodyExpression = lambdaBodyExpression(lambda);
|
|
||||||
if (bodyExpression != null) {
|
|
||||||
String lambdaPayload = extractLambdaBodyPayload(
|
|
||||||
bodyExpression, lambda, transformReceiver, constantResolver, context);
|
|
||||||
if (lambdaPayload != null) {
|
|
||||||
return lambdaPayload;
|
|
||||||
}
|
|
||||||
return extractPayload(bodyExpression, transformReceiver, constantResolver, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return extractPayload(argument, transformReceiver, constantResolver, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractLambdaBodyPayload(
|
|
||||||
Expression bodyExpression,
|
|
||||||
LambdaExpression lambda,
|
|
||||||
Expression transformReceiver,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (!(bodyExpression instanceof MethodInvocation factoryCall)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!LibraryUnwrapRegistry.isReactiveFactoryMethod(factoryCall.getName().getIdentifier())
|
|
||||||
|| factoryCall.arguments().isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Expression factoryArg = (Expression) factoryCall.arguments().get(0);
|
|
||||||
if (!(factoryArg instanceof MethodInvocation getterCall)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String getterSuffix = "." + getterCall.getName().getIdentifier() + "()";
|
|
||||||
Expression getterReceiver = getterCall.getExpression();
|
|
||||||
if (getterReceiver instanceof SimpleName paramName) {
|
|
||||||
String mappedReceiver = mapLambdaParameterToSource(
|
|
||||||
lambda, paramName.getIdentifier(), transformReceiver, constantResolver, context);
|
|
||||||
if (mappedReceiver != null) {
|
|
||||||
return mappedReceiver + getterSuffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (getterReceiver != null) {
|
|
||||||
return getterReceiver.toString() + getterSuffix;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String mapLambdaParameterToSource(
|
|
||||||
LambdaExpression lambda,
|
|
||||||
String paramName,
|
|
||||||
Expression transformReceiver,
|
|
||||||
ConstantResolver constantResolver,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (lambda.parameters().size() != 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Object parameter = lambda.parameters().get(0);
|
|
||||||
if (!(parameter instanceof org.eclipse.jdt.core.dom.VariableDeclaration variableDeclaration)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!paramName.equals(variableDeclaration.getName().getIdentifier())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (transformReceiver instanceof MethodInvocation receiverTransform
|
|
||||||
&& LibraryUnwrapRegistry.isReactiveTransformMethod(receiverTransform.getName().getIdentifier())
|
|
||||||
&& !receiverTransform.arguments().isEmpty()
|
|
||||||
&& receiverTransform.arguments().get(0) instanceof LambdaExpression feederLambda
|
|
||||||
&& feederLambda != lambda) {
|
|
||||||
Expression feederBody = lambdaBodyExpression(feederLambda);
|
|
||||||
if (feederBody != null) {
|
|
||||||
String feederPayload = extractLambdaBodyPayload(
|
|
||||||
feederBody, feederLambda, receiverTransform.getExpression(), constantResolver, context);
|
|
||||||
if (feederPayload != null) {
|
|
||||||
return feederPayload;
|
|
||||||
}
|
|
||||||
String extracted = extractPayload(
|
|
||||||
feederBody, receiverTransform.getExpression(), constantResolver, context);
|
|
||||||
if (extracted != null) {
|
|
||||||
return extracted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Expression source = peelJustArgument(transformReceiver);
|
|
||||||
if (source instanceof MethodInvocation getterMi
|
|
||||||
&& getterMi.getExpression() instanceof SimpleName innerParamName) {
|
|
||||||
LambdaExpression outerLambda = findEnclosingLambda(lambda);
|
|
||||||
if (outerLambda != null && outerLambda != lambda) {
|
|
||||||
MethodInvocation outerTransform = findEnclosingReactiveTransform(outerLambda);
|
|
||||||
if (outerTransform != null) {
|
|
||||||
String mappedBase = mapLambdaParameterToSource(
|
|
||||||
outerLambda,
|
|
||||||
innerParamName.getIdentifier(),
|
|
||||||
outerTransform.getExpression(),
|
|
||||||
constantResolver,
|
|
||||||
context);
|
|
||||||
if (mappedBase != null) {
|
|
||||||
return mappedBase + "." + getterMi.getName().getIdentifier() + "()";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (source != null) {
|
|
||||||
if (constantResolver != null && context != null) {
|
|
||||||
String resolved = constantResolver.resolve(source, context);
|
|
||||||
if (resolved != null) {
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return source.toString();
|
|
||||||
}
|
|
||||||
return extractPayload(transformReceiver, null, constantResolver, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Expression peelJustArgument(Expression expression) {
|
|
||||||
Expression peeled = peelFactoryPayloadExpression(expression);
|
|
||||||
if (peeled != null) {
|
|
||||||
return peeled;
|
|
||||||
}
|
|
||||||
if (expression instanceof MethodInvocation mi && mi.getExpression() != null) {
|
|
||||||
return peelJustArgument(mi.getExpression());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared depth and fallback budget for analysis traversals. Replaces scattered magic-number limits.
|
|
||||||
*/
|
|
||||||
public final class ResolutionBudget {
|
|
||||||
|
|
||||||
public static final int DEFAULT_METHOD_RETURN = 20;
|
|
||||||
public static final int DEFAULT_ACCESSOR_INLINE = 25;
|
|
||||||
public static final int DEFAULT_DATAFLOW = 50;
|
|
||||||
public static final int DEFAULT_UNWRAP = 5;
|
|
||||||
public static final int DEFAULT_CONSTRUCTOR = 10;
|
|
||||||
public static final int DEFAULT_RETURNED_CIC = 50;
|
|
||||||
public static final int DEFAULT_GLOBAL_FIELD_SCAN = 10;
|
|
||||||
|
|
||||||
private final int methodReturnDepth;
|
|
||||||
private final int accessorInlineDepth;
|
|
||||||
private final int dataflowDepth;
|
|
||||||
private final int unwrapDepth;
|
|
||||||
private final int constructorDepth;
|
|
||||||
private final int returnedCicDepth;
|
|
||||||
private final int globalFieldScanLimit;
|
|
||||||
private final boolean allowGlobalFieldScan;
|
|
||||||
|
|
||||||
private ResolutionBudget(
|
|
||||||
int methodReturnDepth,
|
|
||||||
int accessorInlineDepth,
|
|
||||||
int dataflowDepth,
|
|
||||||
int unwrapDepth,
|
|
||||||
int constructorDepth,
|
|
||||||
int returnedCicDepth,
|
|
||||||
int globalFieldScanLimit,
|
|
||||||
boolean allowGlobalFieldScan) {
|
|
||||||
this.methodReturnDepth = methodReturnDepth;
|
|
||||||
this.accessorInlineDepth = accessorInlineDepth;
|
|
||||||
this.dataflowDepth = dataflowDepth;
|
|
||||||
this.unwrapDepth = unwrapDepth;
|
|
||||||
this.constructorDepth = constructorDepth;
|
|
||||||
this.returnedCicDepth = returnedCicDepth;
|
|
||||||
this.globalFieldScanLimit = globalFieldScanLimit;
|
|
||||||
this.allowGlobalFieldScan = allowGlobalFieldScan;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ResolutionBudget defaults() {
|
|
||||||
return new ResolutionBudget(
|
|
||||||
DEFAULT_METHOD_RETURN,
|
|
||||||
DEFAULT_ACCESSOR_INLINE,
|
|
||||||
DEFAULT_DATAFLOW,
|
|
||||||
DEFAULT_UNWRAP,
|
|
||||||
DEFAULT_CONSTRUCTOR,
|
|
||||||
DEFAULT_RETURNED_CIC,
|
|
||||||
DEFAULT_GLOBAL_FIELD_SCAN,
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ResolutionBudget withGlobalFieldScan() {
|
|
||||||
return defaults().enableGlobalFieldScan();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResolutionBudget enableGlobalFieldScan() {
|
|
||||||
return new ResolutionBudget(
|
|
||||||
methodReturnDepth,
|
|
||||||
accessorInlineDepth,
|
|
||||||
dataflowDepth,
|
|
||||||
unwrapDepth,
|
|
||||||
constructorDepth,
|
|
||||||
returnedCicDepth,
|
|
||||||
globalFieldScanLimit,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResolutionBudget child() {
|
|
||||||
return new ResolutionBudget(
|
|
||||||
methodReturnDepth - 1,
|
|
||||||
accessorInlineDepth - 1,
|
|
||||||
dataflowDepth - 1,
|
|
||||||
unwrapDepth,
|
|
||||||
constructorDepth - 1,
|
|
||||||
returnedCicDepth - 1,
|
|
||||||
globalFieldScanLimit,
|
|
||||||
allowGlobalFieldScan);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMethodReturnExhausted(int depth) {
|
|
||||||
return depth > methodReturnDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAccessorInlineExhausted(int depth) {
|
|
||||||
return depth > accessorInlineDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDataflowExhausted(int depth) {
|
|
||||||
return depth > dataflowDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUnwrapExhausted(int depth) {
|
|
||||||
return depth > unwrapDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isConstructorExhausted(int depth) {
|
|
||||||
return depth > constructorDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isReturnedCicExhausted(int depth) {
|
|
||||||
return depth > returnedCicDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int globalFieldScanLimit() {
|
|
||||||
return globalFieldScanLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowGlobalFieldScan() {
|
|
||||||
return allowGlobalFieldScan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,438 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared boolean simplification for transition guards and dispatcher domain checks.
|
|
||||||
*/
|
|
||||||
public final class BooleanConstraintEvaluator {
|
|
||||||
|
|
||||||
private static final Pattern STRING_LITERAL_PATTERN = Pattern.compile("[\"']([a-zA-Z0-9_-]+)[\"']");
|
|
||||||
|
|
||||||
private BooleanConstraintEvaluator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simplifies constraints such as {@code "ORDER".equals(domain)} against a machine domain key.
|
|
||||||
*/
|
|
||||||
public static boolean isCompatibleWithMachineDomain(String constraint, String machineDomainKey) {
|
|
||||||
if (constraint == null || machineDomainKey == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String cleanMachine = machineDomainKey.toUpperCase();
|
|
||||||
String expr = constraint;
|
|
||||||
Set<String> literals = extractStringLiterals(constraint);
|
|
||||||
|
|
||||||
for (String literal : literals) {
|
|
||||||
boolean matchesMachine = literal.equalsIgnoreCase(cleanMachine)
|
|
||||||
|| cleanMachine.equalsIgnoreCase(literal)
|
|
||||||
|| (cleanMachine.length() > literal.length() && cleanMachine.startsWith(literal.toUpperCase()));
|
|
||||||
String replacement = matchesMachine ? "true" : "false";
|
|
||||||
|
|
||||||
String regex1 = "(?i)[\"']?" + Pattern.quote(literal) + "[\"']?\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\([^)]*\\)";
|
|
||||||
String regex2 = "(?i)[a-zA-Z0-9._]+\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\(\\s*[\"']?"
|
|
||||||
+ Pattern.quote(literal) + "[\"']?\\s*\\)";
|
|
||||||
String regex3 = "(?i)[a-zA-Z0-9._]+\\s*==\\s*[\"']?" + Pattern.quote(literal) + "[\"']?";
|
|
||||||
String regex4 = "(?i)[a-zA-Z0-9._]+\\s*!=\\s*[\"']?" + Pattern.quote(literal) + "[\"']?";
|
|
||||||
|
|
||||||
expr = expr.replaceAll(regex1, replacement);
|
|
||||||
expr = expr.replaceAll(regex2, replacement);
|
|
||||||
expr = expr.replaceAll(regex3, replacement);
|
|
||||||
expr = expr.replaceAll(regex4, matchesMachine ? "false" : "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
return evaluateBooleanExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean evaluateBooleanExpression(String expression) {
|
|
||||||
if (expression == null || expression.isBlank()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return parseExpression(expression.replaceAll("\\s+", ""));
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Be conservative in pruning: if we can't parse the expression, do not discard paths.
|
|
||||||
return !expression.contains("false");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates a constraint against known variable bindings gathered while walking a call path forward.
|
|
||||||
* Supports {@code var == VALUE} where VALUE may be a quoted string or enum constant; suffix matching
|
|
||||||
* is used so {@code command == ORDER_PAY} matches {@code DomainCommand.ORDER_PAY}.
|
|
||||||
*/
|
|
||||||
public static boolean isCompatibleWithBindings(String constraint, Map<String, String> bindings) {
|
|
||||||
if (constraint == null || constraint.isBlank()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (bindings == null || bindings.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Set<String> equalityVars = extractEqualityVariables(constraint);
|
|
||||||
for (String varName : equalityVars) {
|
|
||||||
if (!bindings.containsKey(varName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String expr = constraint;
|
|
||||||
for (Map.Entry<String, String> entry : bindings.entrySet()) {
|
|
||||||
if (!isConcreteBindingValue(entry.getKey(), entry.getValue())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
expr = substituteVariableBindings(expr, entry.getKey(), entry.getValue());
|
|
||||||
expr = substituteEqualsLiteralBindings(expr, entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
return evaluateBooleanExpression(expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isConcreteBindingValue(String paramName, String boundValue) {
|
|
||||||
if (boundValue == null || boundValue.isBlank() || boundValue.equals(paramName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (boundValue.contains("(") && !boundValue.contains("valueOf")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (boundValue.startsWith("\"") && boundValue.endsWith("\"")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if ("true".equalsIgnoreCase(boundValue) || "false".equalsIgnoreCase(boundValue)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (boundValue.contains(".") && Character.isUpperCase(boundValue.charAt(boundValue.lastIndexOf('.') + 1))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Treat simple routing keys (e.g. order.pay) as concrete string bindings, but avoid
|
|
||||||
// mistaking variable names (e.g. machineType) for concrete values.
|
|
||||||
if ((boundValue.contains(".") || boundValue.contains("/") || boundValue.contains("-"))
|
|
||||||
&& boundValue.matches("^[a-zA-Z0-9._/-]+$")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return boundValue.equals(boundValue.toUpperCase(Locale.ROOT)) && boundValue.chars().allMatch(ch ->
|
|
||||||
Character.isUpperCase(ch) || ch == '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String substituteEqualsLiteralBindings(String expr, String varName, String boundValue) {
|
|
||||||
if (boundValue == null || boundValue.isBlank()) {
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
String cleanValue = boundValue;
|
|
||||||
if (cleanValue.startsWith("\"") && cleanValue.endsWith("\"")) {
|
|
||||||
cleanValue = cleanValue.substring(1, cleanValue.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
String literalPattern = Pattern.quote(cleanValue);
|
|
||||||
String varToken = Pattern.quote(varName);
|
|
||||||
String argumentEquals = "(?i)" + varToken + "\\s*\\.\\s*(?:equalsIgnoreCase|equals)\\s*\\(\\s*[\"']?"
|
|
||||||
+ literalPattern + "[\"']?\\s*\\)";
|
|
||||||
expr = replaceReceiverEqualsForVariable(expr, varName, cleanValue, "true");
|
|
||||||
expr = replaceObjectsEqualsForVariable(expr, varName, cleanValue, "true");
|
|
||||||
expr = expr.replaceAll(argumentEquals, "true");
|
|
||||||
|
|
||||||
Set<String> allLiterals = extractStringLiterals(expr);
|
|
||||||
for (String literal : allLiterals) {
|
|
||||||
if (literal.equalsIgnoreCase(cleanValue)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String otherLiteral = Pattern.quote(literal);
|
|
||||||
String otherArgument = "(?i)" + varToken + "\\s*\\.\\s*(?:equalsIgnoreCase|equals)\\s*\\(\\s*[\"']?"
|
|
||||||
+ otherLiteral + "[\"']?\\s*\\)";
|
|
||||||
expr = replaceReceiverEqualsForVariable(expr, varName, literal, "false");
|
|
||||||
expr = replaceObjectsEqualsForVariable(expr, varName, literal, "false");
|
|
||||||
expr = expr.replaceAll(otherArgument, "false");
|
|
||||||
}
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String replaceObjectsEqualsForVariable(
|
|
||||||
String expr, String varName, String literalValue, String replacement) {
|
|
||||||
Pattern head = Pattern.compile("(?is)(?:java\\.util\\.)?Objects\\.equals\\s*\\(");
|
|
||||||
Matcher matcher = head.matcher(expr);
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
int cursor = 0;
|
|
||||||
while (matcher.find()) {
|
|
||||||
int matchStart = matcher.start();
|
|
||||||
int openParen = matcher.end() - 1;
|
|
||||||
int closeParen = findMatchingCloseParen(expr, openParen);
|
|
||||||
if (closeParen < 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
String[] args = splitTopLevelComma(expr.substring(openParen + 1, closeParen));
|
|
||||||
result.append(expr, cursor, matchStart);
|
|
||||||
if (args != null
|
|
||||||
&& args.length == 2
|
|
||||||
&& objectsEqualsArgumentMatches(args[0], args[1], varName, literalValue)) {
|
|
||||||
result.append(replacement);
|
|
||||||
} else {
|
|
||||||
result.append(expr, matchStart, closeParen + 1);
|
|
||||||
}
|
|
||||||
cursor = closeParen + 1;
|
|
||||||
}
|
|
||||||
result.append(expr.substring(cursor));
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean objectsEqualsArgumentMatches(
|
|
||||||
String left, String right, String varName, String literalValue) {
|
|
||||||
String leftLiteral = stripConstraintLiteral(left);
|
|
||||||
String rightLiteral = stripConstraintLiteral(right);
|
|
||||||
if (leftLiteral != null
|
|
||||||
&& leftLiteral.equalsIgnoreCase(literalValue)
|
|
||||||
&& constraintArgumentReferencesVariable(right, varName)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return rightLiteral != null
|
|
||||||
&& rightLiteral.equalsIgnoreCase(literalValue)
|
|
||||||
&& constraintArgumentReferencesVariable(left, varName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripConstraintLiteral(String value) {
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String trimmed = value.trim();
|
|
||||||
if (trimmed.length() >= 2
|
|
||||||
&& ((trimmed.startsWith("\"") && trimmed.endsWith("\""))
|
|
||||||
|| (trimmed.startsWith("'") && trimmed.endsWith("'")))) {
|
|
||||||
return trimmed.substring(1, trimmed.length() - 1);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean constraintArgumentReferencesVariable(String argument, String varName) {
|
|
||||||
return argument != null && argument.matches("(?s).*\\b" + Pattern.quote(varName) + "\\b.*");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String[] splitTopLevelComma(String args) {
|
|
||||||
int depth = 0;
|
|
||||||
for (int i = 0; i < args.length(); i++) {
|
|
||||||
char c = args.charAt(i);
|
|
||||||
if (c == '(') {
|
|
||||||
depth++;
|
|
||||||
} else if (c == ')') {
|
|
||||||
depth--;
|
|
||||||
} else if (c == ',' && depth == 0) {
|
|
||||||
return new String[] {args.substring(0, i), args.substring(i + 1)};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String replaceReceiverEqualsForVariable(
|
|
||||||
String expr, String varName, String literalValue, String replacement) {
|
|
||||||
Pattern head = Pattern.compile(
|
|
||||||
"(?is)[\"']?" + Pattern.quote(literalValue) + "[\"']?\\s*\\.\\s*(?:equalsIgnoreCase|equals)\\s*\\(");
|
|
||||||
Matcher matcher = head.matcher(expr);
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
int cursor = 0;
|
|
||||||
while (matcher.find()) {
|
|
||||||
int matchStart = matcher.start();
|
|
||||||
int openParen = matcher.end() - 1;
|
|
||||||
int closeParen = findMatchingCloseParen(expr, openParen);
|
|
||||||
if (closeParen < 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
String argument = expr.substring(openParen + 1, closeParen).trim();
|
|
||||||
result.append(expr, cursor, matchStart);
|
|
||||||
if (argument.equals(varName)
|
|
||||||
|| argument.matches("(?s).*\\b" + Pattern.quote(varName) + "\\b.*")) {
|
|
||||||
result.append(replacement);
|
|
||||||
} else {
|
|
||||||
result.append(expr, matchStart, closeParen + 1);
|
|
||||||
}
|
|
||||||
cursor = closeParen + 1;
|
|
||||||
}
|
|
||||||
result.append(expr.substring(cursor));
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int findMatchingCloseParen(String expr, int openIdx) {
|
|
||||||
int depth = 0;
|
|
||||||
for (int i = openIdx; i < expr.length(); i++) {
|
|
||||||
char c = expr.charAt(i);
|
|
||||||
if (c == '(') {
|
|
||||||
depth++;
|
|
||||||
} else if (c == ')') {
|
|
||||||
depth--;
|
|
||||||
if (depth == 0) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> extractEqualityVariables(String constraint) {
|
|
||||||
Set<String> vars = new HashSet<>();
|
|
||||||
Pattern pattern = Pattern.compile("([a-zA-Z][\\w]*)\\s*==");
|
|
||||||
Matcher matcher = pattern.matcher(constraint);
|
|
||||||
while (matcher.find()) {
|
|
||||||
vars.add(matcher.group(1));
|
|
||||||
}
|
|
||||||
return vars;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String substituteVariableBindings(String expr, String varName, String boundValue) {
|
|
||||||
if (boundValue == null || boundValue.isBlank()) {
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
String cleanValue = boundValue;
|
|
||||||
if (cleanValue.startsWith("\"") && cleanValue.endsWith("\"")) {
|
|
||||||
cleanValue = cleanValue.substring(1, cleanValue.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern eqPattern = Pattern.compile(
|
|
||||||
"(?i)" + Pattern.quote(varName) + "\\s*==\\s*([\\w.\"']+)");
|
|
||||||
Matcher matcher = eqPattern.matcher(expr);
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
while (matcher.find()) {
|
|
||||||
String rhs = matcher.group(1).replace("\"", "").replace("'", "");
|
|
||||||
boolean matches = constraintValuesMatch(cleanValue, rhs);
|
|
||||||
matcher.appendReplacement(sb, matches ? "true" : "false");
|
|
||||||
}
|
|
||||||
matcher.appendTail(sb);
|
|
||||||
if ("true".equalsIgnoreCase(cleanValue) || "false".equalsIgnoreCase(cleanValue)) {
|
|
||||||
Pattern bareVar = Pattern.compile("(?<![\\w.])" + Pattern.quote(varName) + "(?![\\w])");
|
|
||||||
return bareVar.matcher(expr).replaceAll(cleanValue.toLowerCase(Locale.ROOT));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean constraintValuesMatch(String boundValue, String rhs) {
|
|
||||||
if (boundValue == null || rhs == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String bound = stripOuterQuotes(boundValue);
|
|
||||||
String rhsClean = stripOuterQuotes(rhs);
|
|
||||||
if (bound.equals(rhsClean) || bound.equalsIgnoreCase(rhsClean)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (bound.endsWith("." + rhsClean) || rhsClean.endsWith("." + bound)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String boundConstant = enumConstantName(bound);
|
|
||||||
String rhsConstant = enumConstantName(rhsClean);
|
|
||||||
if (!boundConstant.equalsIgnoreCase(rhsConstant)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String boundType = enumTypePart(bound);
|
|
||||||
String rhsType = enumTypePart(rhsClean);
|
|
||||||
if (boundType == null || rhsType == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (boundType.equals(rhsType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String boundSimple = simpleTypeName(boundType);
|
|
||||||
String rhsSimple = simpleTypeName(rhsType);
|
|
||||||
if (!boundSimple.equals(rhsSimple)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
boolean boundImportStyle = !boundType.contains(".");
|
|
||||||
boolean rhsImportStyle = !rhsType.contains(".");
|
|
||||||
return boundImportStyle || rhsImportStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String stripOuterQuotes(String value) {
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
|
|
||||||
return value.substring(1, value.length() - 1);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String enumConstantName(String ref) {
|
|
||||||
int dot = ref.lastIndexOf('.');
|
|
||||||
return dot >= 0 ? ref.substring(dot + 1) : ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String enumTypePart(String ref) {
|
|
||||||
int dot = ref.lastIndexOf('.');
|
|
||||||
if (dot <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ref.substring(0, dot);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String simpleTypeName(String typePart) {
|
|
||||||
int dot = typePart.lastIndexOf('.');
|
|
||||||
return dot >= 0 ? typePart.substring(dot + 1) : typePart;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true}/{@code false} when both sides are compile-time string literals; otherwise {@code null}.
|
|
||||||
*/
|
|
||||||
public static Boolean evaluateStringEquals(String leftLiteral, String rightLiteral, boolean ignoreCase) {
|
|
||||||
if (leftLiteral == null || rightLiteral == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ignoreCase
|
|
||||||
? leftLiteral.equalsIgnoreCase(rightLiteral)
|
|
||||||
: leftLiteral.equals(rightLiteral);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> extractStringLiterals(String constraint) {
|
|
||||||
Set<String> literals = new HashSet<>();
|
|
||||||
Matcher matcher = STRING_LITERAL_PATTERN.matcher(constraint);
|
|
||||||
while (matcher.find()) {
|
|
||||||
literals.add(matcher.group(1));
|
|
||||||
}
|
|
||||||
return literals;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean parseExpression(String expression) {
|
|
||||||
if (expression.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int parenDepth = 0;
|
|
||||||
for (int i = expression.length() - 1; i >= 0; i--) {
|
|
||||||
char c = expression.charAt(i);
|
|
||||||
if (c == ')') {
|
|
||||||
parenDepth++;
|
|
||||||
} else if (c == '(') {
|
|
||||||
parenDepth--;
|
|
||||||
} else if (parenDepth == 0 && i > 0 && expression.charAt(i) == '|' && expression.charAt(i - 1) == '|') {
|
|
||||||
return parseExpression(expression.substring(0, i - 1)) || parseExpression(expression.substring(i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parenDepth = 0;
|
|
||||||
for (int i = expression.length() - 1; i >= 0; i--) {
|
|
||||||
char c = expression.charAt(i);
|
|
||||||
if (c == ')') {
|
|
||||||
parenDepth++;
|
|
||||||
} else if (c == '(') {
|
|
||||||
parenDepth--;
|
|
||||||
} else if (parenDepth == 0 && i > 0 && expression.charAt(i) == '&' && expression.charAt(i - 1) == '&') {
|
|
||||||
return parseExpression(expression.substring(0, i - 1)) && parseExpression(expression.substring(i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expression.startsWith("!")) {
|
|
||||||
return !parseExpression(expression.substring(1));
|
|
||||||
}
|
|
||||||
if (expression.startsWith("(") && expression.endsWith(")")) {
|
|
||||||
return parseExpression(expression.substring(1, expression.length() - 1));
|
|
||||||
}
|
|
||||||
if ("true".equalsIgnoreCase(expression)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if ("false".equalsIgnoreCase(expression)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,557 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.BooleanLiteral;
|
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
|
||||||
import org.eclipse.jdt.core.dom.ConditionalExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.EnumDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.Block;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.ExpressionStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.FieldAccess;
|
|
||||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.QualifiedName;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
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.SwitchCase;
|
|
||||||
import org.eclipse.jdt.core.dom.SwitchExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.SwitchStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.ThisExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
|
||||||
import org.eclipse.jdt.core.dom.YieldStatement;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates no-arg boolean member calls in dispatcher constraints (e.g. {@code eventType.isEvent()})
|
|
||||||
* against concrete enum constants by reading enum source — no hard-coded predicate names.
|
|
||||||
*/
|
|
||||||
public final class EnumMemberPredicateEvaluator {
|
|
||||||
|
|
||||||
private static final Pattern PREDICATE_PART =
|
|
||||||
Pattern.compile("!?\\s*([a-zA-Z][\\w]*)\\s*\\.\\s*([a-zA-Z][\\w]*)\\s*\\(\\s*\\)");
|
|
||||||
|
|
||||||
private EnumMemberPredicateEvaluator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record PredicateCall(String receiverName, String methodName, boolean negated) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasEnumMemberPredicates(String constraint) {
|
|
||||||
return !extractPredicateCalls(constraint).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<PredicateCall> extractPredicateCalls(String constraint) {
|
|
||||||
if (constraint == null || constraint.isBlank()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<PredicateCall> calls = new ArrayList<>();
|
|
||||||
for (String part : splitTopLevelAndParts(constraint)) {
|
|
||||||
Matcher matcher = PREDICATE_PART.matcher(part.trim());
|
|
||||||
if (matcher.matches()) {
|
|
||||||
boolean negated = part.trim().startsWith("!");
|
|
||||||
calls.add(new PredicateCall(matcher.group(1), matcher.group(2), negated));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return calls;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> filterEnumConstants(
|
|
||||||
List<String> candidates,
|
|
||||||
String constraint,
|
|
||||||
String enumTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (candidates == null || candidates.isEmpty()) {
|
|
||||||
return candidates == null ? List.of() : candidates;
|
|
||||||
}
|
|
||||||
List<PredicateCall> predicates = extractPredicateCalls(constraint);
|
|
||||||
if (predicates.isEmpty()) {
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
EnumDeclaration enumDecl = findEnumDeclaration(enumTypeFqn, context);
|
|
||||||
if (enumDecl == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> filtered = new ArrayList<>();
|
|
||||||
for (String candidate : candidates) {
|
|
||||||
if (satisfiesPredicates(candidate, predicates, enumDecl, context)) {
|
|
||||||
filtered.add(candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True when every predicate method in the constraint exists on the enum (or its interfaces).
|
|
||||||
* Used to distinguish inconclusive filtering from an intentional empty result.
|
|
||||||
*/
|
|
||||||
public static boolean hasResolvablePredicateMethods(
|
|
||||||
String constraint,
|
|
||||||
String enumTypeFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
List<PredicateCall> predicates = extractPredicateCalls(constraint);
|
|
||||||
if (predicates.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
EnumDeclaration enumDecl = findEnumDeclaration(enumTypeFqn, context);
|
|
||||||
if (enumDecl == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (PredicateCall predicate : predicates) {
|
|
||||||
MethodDeclaration method = findParameterlessMethod(enumDecl, predicate.methodName());
|
|
||||||
if (method == null && context != null) {
|
|
||||||
method = findInterfaceParameterlessMethod(enumDecl, predicate.methodName(), context);
|
|
||||||
}
|
|
||||||
if (method == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean satisfiesPredicates(
|
|
||||||
String canonicalConstantFqn,
|
|
||||||
List<PredicateCall> predicates,
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
CodebaseContext context) {
|
|
||||||
String constantName = constantSimpleName(canonicalConstantFqn);
|
|
||||||
if (constantName == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Map<String, Boolean> boolFields = resolveConstantBooleanFields(enumDecl, constantName);
|
|
||||||
for (PredicateCall predicate : predicates) {
|
|
||||||
Boolean methodResult = evaluateBooleanMethod(
|
|
||||||
enumDecl, predicate.methodName(), constantName, boolFields, context);
|
|
||||||
if (methodResult == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
boolean expected = predicate.negated ? !methodResult : methodResult;
|
|
||||||
if (!expected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Boolean evaluateBooleanMethod(
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
String methodName,
|
|
||||||
String currentConstantName,
|
|
||||||
Map<String, Boolean> boolFields,
|
|
||||||
CodebaseContext context) {
|
|
||||||
MethodDeclaration method = findParameterlessMethod(enumDecl, methodName);
|
|
||||||
if (method == null && context != null) {
|
|
||||||
method = findInterfaceParameterlessMethod(enumDecl, methodName, context);
|
|
||||||
}
|
|
||||||
if (method == null || method.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Boolean switchResult = evaluateSwitchDrivenMethod(
|
|
||||||
method, currentConstantName, boolFields, enumDecl, context);
|
|
||||||
if (switchResult != null) {
|
|
||||||
return switchResult;
|
|
||||||
}
|
|
||||||
Boolean[] result = new Boolean[1];
|
|
||||||
method.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (result[0] == null) {
|
|
||||||
result[0] = evaluateBooleanExpression(
|
|
||||||
node.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean evaluateSwitchDrivenMethod(
|
|
||||||
MethodDeclaration method,
|
|
||||||
String currentConstantName,
|
|
||||||
Map<String, Boolean> boolFields,
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
CodebaseContext context) {
|
|
||||||
Boolean[] result = new Boolean[1];
|
|
||||||
method.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(SwitchStatement node) {
|
|
||||||
if (result[0] == null && isEnumSelfSelector(node.getExpression())) {
|
|
||||||
result[0] = evaluateSwitchLike(
|
|
||||||
node.statements(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
return result[0] == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean visit(SwitchExpression node) {
|
|
||||||
if (result[0] == null && isEnumSelfSelector(node.getExpression())) {
|
|
||||||
result[0] = evaluateSwitchLike(
|
|
||||||
node.statements(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
return result[0] == null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, Boolean> resolveConstantBooleanFields(EnumDeclaration enumDecl, String constantName) {
|
|
||||||
Map<String, Boolean> fields = new HashMap<>();
|
|
||||||
EnumConstantDeclaration constant = findEnumConstant(enumDecl, constantName);
|
|
||||||
if (constant == null) {
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
MethodDeclaration constructor = findEnumConstructor(enumDecl);
|
|
||||||
if (constructor != null && constant.arguments().size() == constructor.parameters().size()) {
|
|
||||||
for (int i = 0; i < constructor.parameters().size(); i++) {
|
|
||||||
SingleVariableDeclaration param =
|
|
||||||
(SingleVariableDeclaration) constructor.parameters().get(i);
|
|
||||||
Expression arg = (Expression) constant.arguments().get(i);
|
|
||||||
Boolean boolVal = literalBoolean(arg);
|
|
||||||
if (boolVal != null) {
|
|
||||||
fields.put(param.getName().getIdentifier(), boolVal);
|
|
||||||
mapFieldName(enumDecl, param.getName().getIdentifier(), boolVal, fields);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Object bodyObj : enumDecl.bodyDeclarations()) {
|
|
||||||
if (bodyObj instanceof FieldDeclaration fd) {
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
|
||||||
Expression init = fragment.getInitializer();
|
|
||||||
Boolean boolVal = literalBoolean(init);
|
|
||||||
if (boolVal != null) {
|
|
||||||
fields.put(fragment.getName().getIdentifier(), boolVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void mapFieldName(
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
String paramName,
|
|
||||||
Boolean value,
|
|
||||||
Map<String, Boolean> fields) {
|
|
||||||
for (Object bodyObj : enumDecl.bodyDeclarations()) {
|
|
||||||
if (!(bodyObj instanceof FieldDeclaration fd)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (Object fragObj : fd.fragments()) {
|
|
||||||
if (fragObj instanceof VariableDeclarationFragment fragment
|
|
||||||
&& fragment.getName().getIdentifier().equals(paramName)) {
|
|
||||||
fields.put(paramName, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean evaluateBooleanExpression(
|
|
||||||
Expression expr,
|
|
||||||
String currentConstantName,
|
|
||||||
Map<String, Boolean> boolFields,
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (expr instanceof BooleanLiteral bl) {
|
|
||||||
return bl.booleanValue();
|
|
||||||
}
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
return boolFields.get(sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (expr instanceof FieldAccess fa) {
|
|
||||||
if (fa.getExpression() == null || fa.getExpression() instanceof ThisExpression) {
|
|
||||||
return boolFields.get(fa.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expr instanceof ParenthesizedExpression pe) {
|
|
||||||
return evaluateBooleanExpression(
|
|
||||||
pe.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
if (expr instanceof ConditionalExpression ce) {
|
|
||||||
Boolean thenVal = evaluateBooleanExpression(
|
|
||||||
ce.getThenExpression(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
Boolean elseVal = evaluateBooleanExpression(
|
|
||||||
ce.getElseExpression(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
if (thenVal != null && elseVal != null && thenVal.equals(elseVal)) {
|
|
||||||
return thenVal;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (expr instanceof SwitchExpression se && isEnumSelfSelector(se.getExpression())) {
|
|
||||||
return evaluateSwitchLike(se.statements(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
if (expr instanceof MethodInvocation mi
|
|
||||||
&& mi.arguments().isEmpty()
|
|
||||||
&& enumDecl != null
|
|
||||||
&& context != null) {
|
|
||||||
return evaluateBooleanMethod(
|
|
||||||
enumDecl, mi.getName().getIdentifier(), currentConstantName, boolFields, context);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean evaluateSwitchLike(
|
|
||||||
List<?> statements,
|
|
||||||
String currentConstantName,
|
|
||||||
Map<String, Boolean> boolFields,
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
CodebaseContext context) {
|
|
||||||
List<String> caseConstants = new ArrayList<>();
|
|
||||||
boolean defaultCase = false;
|
|
||||||
boolean sawArmBody = false;
|
|
||||||
boolean pendingCase = false;
|
|
||||||
for (Object statementObj : statements) {
|
|
||||||
if (statementObj instanceof SwitchCase switchCase) {
|
|
||||||
if (sawArmBody) {
|
|
||||||
caseConstants.clear();
|
|
||||||
defaultCase = false;
|
|
||||||
sawArmBody = false;
|
|
||||||
}
|
|
||||||
pendingCase = true;
|
|
||||||
if (switchCase.isDefault()) {
|
|
||||||
defaultCase = true;
|
|
||||||
} else {
|
|
||||||
for (Object exprObj : switchCase.expressions()) {
|
|
||||||
if (exprObj instanceof Expression caseExpr) {
|
|
||||||
String caseConstant = extractCaseConstantName(caseExpr);
|
|
||||||
if (caseConstant != null) {
|
|
||||||
caseConstants.add(caseConstant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!pendingCase) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sawArmBody = true;
|
|
||||||
if (!defaultCase && !caseConstants.contains(currentConstantName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Boolean armValue = evaluateSwitchArmNode(
|
|
||||||
statementObj, currentConstantName, boolFields, enumDecl, context);
|
|
||||||
if (armValue != null) {
|
|
||||||
return armValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean evaluateSwitchArmNode(
|
|
||||||
Object node,
|
|
||||||
String currentConstantName,
|
|
||||||
Map<String, Boolean> boolFields,
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (node instanceof ReturnStatement rs) {
|
|
||||||
return evaluateBooleanExpression(
|
|
||||||
rs.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
if (node instanceof YieldStatement ys) {
|
|
||||||
return evaluateBooleanExpression(
|
|
||||||
ys.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
if (node instanceof ExpressionStatement es) {
|
|
||||||
return evaluateBooleanExpression(
|
|
||||||
es.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
if (node instanceof Block block) {
|
|
||||||
return evaluateLinearBooleanStatements(
|
|
||||||
block.statements(), currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
if (node instanceof Expression expression) {
|
|
||||||
return evaluateBooleanExpression(
|
|
||||||
expression, currentConstantName, boolFields, enumDecl, context);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean evaluateLinearBooleanStatements(
|
|
||||||
List<?> statements,
|
|
||||||
String currentConstantName,
|
|
||||||
Map<String, Boolean> boolFields,
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
CodebaseContext context) {
|
|
||||||
for (Object statementObj : statements) {
|
|
||||||
Boolean value = evaluateSwitchArmNode(
|
|
||||||
statementObj, currentConstantName, boolFields, enumDecl, context);
|
|
||||||
if (value != null) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEnumSelfSelector(Expression expr) {
|
|
||||||
if (expr instanceof ThisExpression) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (expr instanceof ParenthesizedExpression pe) {
|
|
||||||
return isEnumSelfSelector(pe.getExpression());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractCaseConstantName(Expression expr) {
|
|
||||||
if (expr instanceof SimpleName simpleName) {
|
|
||||||
return simpleName.getIdentifier();
|
|
||||||
}
|
|
||||||
if (expr instanceof QualifiedName qualifiedName) {
|
|
||||||
return qualifiedName.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
if (expr instanceof FieldAccess fieldAccess) {
|
|
||||||
return fieldAccess.getName().getIdentifier();
|
|
||||||
}
|
|
||||||
if (expr instanceof ParenthesizedExpression pe) {
|
|
||||||
return extractCaseConstantName(pe.getExpression());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean literalBoolean(Expression expr) {
|
|
||||||
return expr instanceof BooleanLiteral bl ? bl.booleanValue() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MethodDeclaration findParameterlessMethod(AbstractTypeDeclaration typeDecl, String methodName) {
|
|
||||||
if (typeDecl == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (Object bodyObj : typeDecl.bodyDeclarations()) {
|
|
||||||
if (bodyObj instanceof MethodDeclaration md
|
|
||||||
&& !md.isConstructor()
|
|
||||||
&& methodName.equals(md.getName().getIdentifier())
|
|
||||||
&& md.parameters().isEmpty()) {
|
|
||||||
return md;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MethodDeclaration findInterfaceParameterlessMethod(
|
|
||||||
EnumDeclaration enumDecl,
|
|
||||||
String methodName,
|
|
||||||
CodebaseContext context) {
|
|
||||||
CompilationUnit cu = enumDecl.getRoot() instanceof CompilationUnit root ? root : null;
|
|
||||||
for (Object typeObj : enumDecl.superInterfaceTypes()) {
|
|
||||||
if (!(typeObj instanceof SimpleType simpleType)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String interfaceName = simpleType.getName().getFullyQualifiedName();
|
|
||||||
TypeDeclaration interfaceDecl = context.getTypeDeclaration(interfaceName, cu);
|
|
||||||
MethodDeclaration method = findParameterlessMethod(interfaceDecl, methodName);
|
|
||||||
if (method != null) {
|
|
||||||
return method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MethodDeclaration findEnumConstructor(EnumDeclaration enumDecl) {
|
|
||||||
MethodDeclaration implicit = null;
|
|
||||||
for (Object bodyObj : enumDecl.bodyDeclarations()) {
|
|
||||||
if (bodyObj instanceof MethodDeclaration md && md.isConstructor()) {
|
|
||||||
if (md.parameters().isEmpty()) {
|
|
||||||
implicit = md;
|
|
||||||
} else {
|
|
||||||
return md;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return implicit;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EnumConstantDeclaration findEnumConstant(EnumDeclaration enumDecl, String constantName) {
|
|
||||||
for (Object constantObj : enumDecl.enumConstants()) {
|
|
||||||
if (constantObj instanceof EnumConstantDeclaration ecd
|
|
||||||
&& constantName.equals(ecd.getName().getIdentifier())) {
|
|
||||||
return ecd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static EnumDeclaration findEnumDeclaration(String enumTypeFqn, CodebaseContext context) {
|
|
||||||
if (enumTypeFqn == null || context == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String clean = enumTypeFqn.contains("<") ? enumTypeFqn.substring(0, enumTypeFqn.indexOf('<')) : enumTypeFqn;
|
|
||||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
|
||||||
String pkg = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
|
||||||
for (Object typeObj : cu.types()) {
|
|
||||||
EnumDeclaration found = findEnumNode(typeObj, clean, pkg, context);
|
|
||||||
if (found != null) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EnumDeclaration findEnumNode(Object typeObj, String targetFqn, String pkg, CodebaseContext context) {
|
|
||||||
if (typeObj instanceof EnumDeclaration ed) {
|
|
||||||
String fqn = pkg.isEmpty() ? ed.getName().getIdentifier() : pkg + "." + ed.getName().getIdentifier();
|
|
||||||
if (targetFqn.equals(fqn)) {
|
|
||||||
return ed;
|
|
||||||
}
|
|
||||||
} else if (typeObj instanceof TypeDeclaration td) {
|
|
||||||
String parentFqn = pkg.isEmpty() ? td.getName().getIdentifier() : pkg + "." + td.getName().getIdentifier();
|
|
||||||
for (Object decl : td.bodyDeclarations()) {
|
|
||||||
if (decl instanceof EnumDeclaration nested) {
|
|
||||||
String nestedFqn = parentFqn + "." + nested.getName().getIdentifier();
|
|
||||||
if (targetFqn.equals(nestedFqn)) {
|
|
||||||
return nested;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String constantSimpleName(String canonicalConstantFqn) {
|
|
||||||
if (canonicalConstantFqn == null || !canonicalConstantFqn.contains(".")) {
|
|
||||||
return canonicalConstantFqn;
|
|
||||||
}
|
|
||||||
return canonicalConstantFqn.substring(canonicalConstantFqn.lastIndexOf('.') + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> splitTopLevelAndParts(String constraint) {
|
|
||||||
List<String> parts = new ArrayList<>();
|
|
||||||
StringBuilder current = new StringBuilder();
|
|
||||||
int depth = 0;
|
|
||||||
for (int i = 0; i < constraint.length(); i++) {
|
|
||||||
char c = constraint.charAt(i);
|
|
||||||
if (c == '(') {
|
|
||||||
depth++;
|
|
||||||
} else if (c == ')') {
|
|
||||||
depth--;
|
|
||||||
} else if (depth == 0 && i + 1 < constraint.length()
|
|
||||||
&& c == '&' && constraint.charAt(i + 1) == '&') {
|
|
||||||
parts.add(current.toString());
|
|
||||||
current.setLength(0);
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
current.append(c);
|
|
||||||
}
|
|
||||||
if (!current.isEmpty()) {
|
|
||||||
parts.add(current.toString());
|
|
||||||
}
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
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.Collection;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discovers generated {@code .java} sources under Maven {@code target/} and Gradle {@code build/} when
|
|
||||||
* they already exist in the workspace. Does not run Maven or Gradle.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public final class GeneratedSourceDiscovery {
|
|
||||||
|
|
||||||
private static final List<String> BUILD_ROOTS = List.of("target", "build");
|
|
||||||
|
|
||||||
private static final List<String> EXCLUDED_RELATIVE_SEGMENTS = List.of(
|
|
||||||
"classes",
|
|
||||||
"test-classes",
|
|
||||||
"generated-test-sources",
|
|
||||||
"generated-test",
|
|
||||||
"test",
|
|
||||||
"testfixtures",
|
|
||||||
"test-fixtures",
|
|
||||||
"androidtest",
|
|
||||||
"inttest",
|
|
||||||
"functionaltest",
|
|
||||||
"e2etest",
|
|
||||||
"libs",
|
|
||||||
"lib",
|
|
||||||
"tmp",
|
|
||||||
"reports",
|
|
||||||
"test-results",
|
|
||||||
"surefire",
|
|
||||||
"failsafe",
|
|
||||||
"surefire-reports",
|
|
||||||
"failsafe-reports",
|
|
||||||
"cache",
|
|
||||||
"caches",
|
|
||||||
"outputs",
|
|
||||||
"intermediates",
|
|
||||||
"kotlin",
|
|
||||||
"native",
|
|
||||||
"resources",
|
|
||||||
"distributions",
|
|
||||||
"docs",
|
|
||||||
"javadoc",
|
|
||||||
"testOutput");
|
|
||||||
|
|
||||||
public Set<Path> discoverSourceRoots(Collection<Path> moduleDirectories) {
|
|
||||||
Set<Path> roots = new LinkedHashSet<>();
|
|
||||||
if (moduleDirectories == null) {
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
for (Path moduleDir : moduleDirectories) {
|
|
||||||
if (moduleDir == null || !Files.isDirectory(moduleDir)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
roots.addAll(discoverSourceRoots(moduleDir));
|
|
||||||
}
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> discoverSourceRoots(Path moduleDirectory) {
|
|
||||||
Set<Path> roots = new LinkedHashSet<>();
|
|
||||||
Path normalizedModule = moduleDirectory.toAbsolutePath().normalize();
|
|
||||||
for (String buildRootName : BUILD_ROOTS) {
|
|
||||||
Path buildRoot = normalizedModule.resolve(buildRootName);
|
|
||||||
if (containsGeneratedJava(normalizedModule, buildRoot)) {
|
|
||||||
roots.add(buildRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!roots.isEmpty()) {
|
|
||||||
log.info("Discovered generated source roots under {}: {}", normalizedModule, roots);
|
|
||||||
}
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasGeneratedSources(Path moduleDirectory) {
|
|
||||||
return !discoverSourceRoots(moduleDirectory).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean containsGeneratedJava(Path moduleDirectory, Path buildRoot) {
|
|
||||||
if (!Files.isDirectory(buildRoot)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try (Stream<Path> files = Files.walk(buildRoot)) {
|
|
||||||
return files.filter(Files::isRegularFile)
|
|
||||||
.anyMatch(path -> path.getFileName().toString().endsWith(".java")
|
|
||||||
&& !isExcludedBuildPath(moduleDirectory, path));
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.debug("Failed to scan generated sources under {}", buildRoot, e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isExcludedBuildPath(Path moduleDirectory, Path candidate) {
|
|
||||||
Path normalizedModule = moduleDirectory.toAbsolutePath().normalize();
|
|
||||||
Path normalizedCandidate = candidate.toAbsolutePath().normalize();
|
|
||||||
if (!normalizedCandidate.startsWith(normalizedModule)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Path relative = normalizedModule.relativize(normalizedCandidate);
|
|
||||||
if (relative.getNameCount() < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String buildRoot = relative.getName(0).toString().toLowerCase(Locale.ROOT);
|
|
||||||
if (!buildRoot.equals("target") && !buildRoot.equals("build")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (int i = 1; i < relative.getNameCount(); i++) {
|
|
||||||
String segment = relative.getName(i).toString().toLowerCase(Locale.ROOT);
|
|
||||||
if (isExcludedBuildSegment(segment)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isExcludedBuildSegment(String segment) {
|
|
||||||
if (segment == null || segment.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String lower = segment.toLowerCase(Locale.ROOT);
|
|
||||||
if (EXCLUDED_RELATIVE_SEGMENTS.contains(lower)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return lower.contains("test-sources")
|
|
||||||
|| lower.startsWith("test-")
|
|
||||||
|| lower.endsWith("-test")
|
|
||||||
|| lower.contains("-test-");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiFunction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile-time literal folding for map lookups, array indexing, and narrow string transforms.
|
|
||||||
*/
|
|
||||||
final class LiteralExpressionSupport {
|
|
||||||
|
|
||||||
private LiteralExpressionSupport() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveArrayAccess(
|
|
||||||
ArrayAccess access,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
Integer index = resolveLiteralInt(access.getIndex(), context, visited, resolver);
|
|
||||||
if (index == null || index < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<String> elements = extractArrayElements(access.getArray(), context, visited, resolver);
|
|
||||||
if (elements == null || index >= elements.size()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return elements.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveMapGet(
|
|
||||||
MethodInvocation getCall,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (getCall.arguments().isEmpty() || getCall.getExpression() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String key = resolver.apply((Expression) getCall.arguments().get(0), visited);
|
|
||||||
if (key == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, String> entries = extractMapEntries(getCall.getExpression(), context, visited, resolver);
|
|
||||||
if (entries == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return entries.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveLiteralStringTransform(
|
|
||||||
MethodInvocation transformCall,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (!transformCall.arguments().isEmpty() || transformCall.getExpression() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String receiver = resolver.apply(transformCall.getExpression(), visited);
|
|
||||||
if (receiver == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return switch (transformCall.getName().getIdentifier()) {
|
|
||||||
case "toUpperCase" -> receiver.toUpperCase(Locale.ROOT);
|
|
||||||
case "toLowerCase" -> receiver.toLowerCase(Locale.ROOT);
|
|
||||||
case "trim" -> receiver.trim();
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Integer resolveLiteralInt(
|
|
||||||
Expression expr,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (expr instanceof NumberLiteral nl) {
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(nl.getToken().replace("_", ""));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expr instanceof ParenthesizedExpression pe) {
|
|
||||||
return resolveLiteralInt(pe.getExpression(), context, visited, resolver);
|
|
||||||
}
|
|
||||||
if (expr instanceof InfixExpression infix) {
|
|
||||||
Integer left = resolveLiteralInt(infix.getLeftOperand(), context, visited, resolver);
|
|
||||||
Integer right = resolveLiteralInt(infix.getRightOperand(), context, visited, resolver);
|
|
||||||
if (left == null || right == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
InfixExpression.Operator op = infix.getOperator();
|
|
||||||
if (op == InfixExpression.Operator.PLUS) {
|
|
||||||
return left + right;
|
|
||||||
}
|
|
||||||
if (op == InfixExpression.Operator.MINUS) {
|
|
||||||
return left - right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String resolved = resolver.apply(expr, visited);
|
|
||||||
if (resolved != null) {
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(resolved.replace("_", ""));
|
|
||||||
} catch (NumberFormatException ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, String> extractMapEntries(
|
|
||||||
Expression expr,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
Map<String, String> fromFactory = extractMapFactoryEntries(mi, context, visited, resolver);
|
|
||||||
if (fromFactory != null) {
|
|
||||||
return fromFactory;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String resolvedField = resolver.apply(expr, visited);
|
|
||||||
if (resolvedField != null && resolvedField.startsWith("MAP:")) {
|
|
||||||
return parseMapLiteral(resolvedField);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extractMapFactoryEntries(
|
|
||||||
MethodInvocation mi,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (!isMapFactory(mi.getExpression())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String method = mi.getName().getIdentifier();
|
|
||||||
if ("of".equals(method)) {
|
|
||||||
return extractAlternatingPairs(mi.arguments(), context, visited, resolver);
|
|
||||||
}
|
|
||||||
if ("ofEntries".equals(method)) {
|
|
||||||
Map<String, String> entries = new LinkedHashMap<>();
|
|
||||||
for (Object argObj : mi.arguments()) {
|
|
||||||
if (argObj instanceof Expression entryExpr) {
|
|
||||||
Map<String, String> single = extractMapEntryCall(entryExpr, context, visited, resolver);
|
|
||||||
if (single != null) {
|
|
||||||
entries.putAll(single);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entries.isEmpty() ? null : entries;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extractMapEntryCall(
|
|
||||||
Expression expr,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (!(expr instanceof MethodInvocation entryCall) || !"entry".equals(entryCall.getName().getIdentifier())) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!isMapFactory(entryCall.getExpression()) || entryCall.arguments().size() != 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String key = resolver.apply((Expression) entryCall.arguments().get(0), visited);
|
|
||||||
String value = resolver.apply((Expression) entryCall.arguments().get(1), visited);
|
|
||||||
if (key == null || value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Map.of(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extractAlternatingPairs(
|
|
||||||
List<?> arguments,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (arguments.size() < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, String> entries = new LinkedHashMap<>();
|
|
||||||
for (int i = 0; i + 1 < arguments.size(); i += 2) {
|
|
||||||
String key = resolver.apply((Expression) arguments.get(i), visited);
|
|
||||||
String value = resolver.apply((Expression) arguments.get(i + 1), visited);
|
|
||||||
if (key == null || value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
entries.put(key, value);
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String encodeMapLiteral(Map<String, String> entries) {
|
|
||||||
StringBuilder sb = new StringBuilder("MAP:");
|
|
||||||
boolean first = true;
|
|
||||||
for (Map.Entry<String, String> entry : entries.entrySet()) {
|
|
||||||
if (!first) {
|
|
||||||
sb.append('|');
|
|
||||||
}
|
|
||||||
first = false;
|
|
||||||
sb.append(escape(entry.getKey())).append('=').append(escape(entry.getValue()));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, String> parseMapLiteral(String encoded) {
|
|
||||||
if (encoded == null || !encoded.startsWith("MAP:")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, String> entries = new LinkedHashMap<>();
|
|
||||||
for (String pair : encoded.substring(4).split("\\|")) {
|
|
||||||
int eq = pair.indexOf('=');
|
|
||||||
if (eq <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entries.put(unescape(pair.substring(0, eq)), unescape(pair.substring(eq + 1)));
|
|
||||||
}
|
|
||||||
return entries.isEmpty() ? null : entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<String> mapLiteralValues(String encoded) {
|
|
||||||
Map<String, String> entries = parseMapLiteral(encoded);
|
|
||||||
if (entries == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return List.copyOf(entries.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String escape(String value) {
|
|
||||||
return value.replace("\\", "\\\\").replace("|", "\\|").replace("=", "\\=");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String unescape(String value) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (int i = 0; i < value.length(); i++) {
|
|
||||||
char c = value.charAt(i);
|
|
||||||
if (c == '\\' && i + 1 < value.length()) {
|
|
||||||
sb.append(value.charAt(++i));
|
|
||||||
} else {
|
|
||||||
sb.append(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> extractArrayElements(
|
|
||||||
Expression arrayExpr,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
if (arrayExpr instanceof ArrayCreation creation && creation.getInitializer() != null) {
|
|
||||||
return extractExpressionList(creation.getInitializer().expressions(), context, visited, resolver);
|
|
||||||
}
|
|
||||||
if (arrayExpr instanceof ArrayInitializer initializer) {
|
|
||||||
return extractExpressionList(initializer.expressions(), context, visited, resolver);
|
|
||||||
}
|
|
||||||
String resolved = resolver.apply(arrayExpr, visited);
|
|
||||||
if (resolved != null && resolved.startsWith("ARRAY:")) {
|
|
||||||
return List.of(resolved.substring(6).split("\\|", -1));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String encodeArrayLiteral(List<String> values) {
|
|
||||||
return "ARRAY:" + String.join("|", values.stream().map(LiteralExpressionSupport::escape).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> extractExpressionList(
|
|
||||||
List<?> expressions,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
BiFunction<Expression, Set<String>, String> resolver) {
|
|
||||||
List<String> values = new ArrayList<>();
|
|
||||||
for (Object expressionObj : expressions) {
|
|
||||||
String value = resolver.apply((Expression) expressionObj, visited);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
values.add(value);
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isMapFactory(Expression expression) {
|
|
||||||
if (expression == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (expression instanceof SimpleName sn) {
|
|
||||||
return "Map".equals(sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (expression instanceof QualifiedName qn) {
|
|
||||||
return "Map".equals(qn.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,259 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static wiring model built from parsed {@code settings.gradle}, {@code build.gradle}, and {@code pom.xml}
|
|
||||||
* files — no Maven/Gradle execution.
|
|
||||||
*/
|
|
||||||
public final class ProjectModuleGraph {
|
|
||||||
|
|
||||||
public enum BuildSystem {
|
|
||||||
GRADLE,
|
|
||||||
MAVEN,
|
|
||||||
MIXED
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum DependencyKind {
|
|
||||||
LOCAL_PROJECT,
|
|
||||||
EXTERNAL_LIBRARY
|
|
||||||
}
|
|
||||||
|
|
||||||
public record MavenCoordinates(String groupId, String artifactId) {
|
|
||||||
public MavenCoordinates {
|
|
||||||
Objects.requireNonNull(groupId, "groupId");
|
|
||||||
Objects.requireNonNull(artifactId, "artifactId");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String key() {
|
|
||||||
return groupId + ":" + artifactId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ProjectModule(
|
|
||||||
String id,
|
|
||||||
Path directory,
|
|
||||||
BuildSystem buildSystem,
|
|
||||||
MavenCoordinates mavenCoordinates) {
|
|
||||||
|
|
||||||
public Optional<MavenCoordinates> coordinates() {
|
|
||||||
return Optional.ofNullable(mavenCoordinates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ModuleDependency(
|
|
||||||
String fromModuleId,
|
|
||||||
String toModuleId,
|
|
||||||
MavenCoordinates externalCoordinates,
|
|
||||||
DependencyKind kind) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Path workspaceRoot;
|
|
||||||
private final Map<String, ProjectModule> modulesById;
|
|
||||||
private final Map<MavenCoordinates, ProjectModule> modulesByCoordinates;
|
|
||||||
private final List<ModuleDependency> dependencies;
|
|
||||||
|
|
||||||
public ProjectModuleGraph(
|
|
||||||
Path workspaceRoot,
|
|
||||||
Map<String, ProjectModule> modulesById,
|
|
||||||
Map<MavenCoordinates, ProjectModule> modulesByCoordinates,
|
|
||||||
List<ModuleDependency> dependencies) {
|
|
||||||
this.workspaceRoot = workspaceRoot != null ? workspaceRoot.normalize() : null;
|
|
||||||
this.modulesById = Map.copyOf(modulesById);
|
|
||||||
this.modulesByCoordinates = Map.copyOf(modulesByCoordinates);
|
|
||||||
this.dependencies = List.copyOf(dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path workspaceRoot() {
|
|
||||||
return workspaceRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, ProjectModule> modulesById() {
|
|
||||||
return modulesById;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<MavenCoordinates, ProjectModule> modulesByCoordinates() {
|
|
||||||
return modulesByCoordinates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ModuleDependency> dependencies() {
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<ProjectModule> findByDirectory(Path directory) {
|
|
||||||
if (directory == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Path normalized = directory.toAbsolutePath().normalize();
|
|
||||||
return modulesById.values().stream()
|
|
||||||
.filter(module -> module.directory().toAbsolutePath().normalize().equals(normalized))
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<ProjectModule> findContainingModule(Path path) {
|
|
||||||
if (path == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Path normalized = path.toAbsolutePath().normalize();
|
|
||||||
ProjectModule best = null;
|
|
||||||
int bestLength = -1;
|
|
||||||
for (ProjectModule module : modulesById.values()) {
|
|
||||||
Path moduleDir = module.directory().toAbsolutePath().normalize();
|
|
||||||
if (normalized.startsWith(moduleDir) && moduleDir.getNameCount() > bestLength) {
|
|
||||||
best = module;
|
|
||||||
bestLength = moduleDir.getNameCount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.ofNullable(best);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> allModuleDirectories() {
|
|
||||||
Set<Path> paths = new LinkedHashSet<>();
|
|
||||||
for (ProjectModule module : modulesById.values()) {
|
|
||||||
if (Files.isDirectory(module.directory())) {
|
|
||||||
paths.add(module.directory().toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isGradleAggregatorRoot(Path directory) {
|
|
||||||
if (directory == null || workspaceRoot == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return directory.toAbsolutePath().normalize().equals(workspaceRoot)
|
|
||||||
&& modulesById.values().stream().anyMatch(module -> module.buildSystem() == BuildSystem.GRADLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMavenAggregatorRoot(Path directory) {
|
|
||||||
if (directory == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Path normalized = directory.toAbsolutePath().normalize();
|
|
||||||
long childModules = modulesById.values().stream()
|
|
||||||
.filter(module -> module.directory().toAbsolutePath().normalize().startsWith(normalized))
|
|
||||||
.filter(module -> !module.directory().toAbsolutePath().normalize().equals(normalized))
|
|
||||||
.count();
|
|
||||||
return childModules > 0 && findByDirectory(normalized)
|
|
||||||
.map(module -> module.buildSystem() == BuildSystem.MAVEN || module.buildSystem() == BuildSystem.MIXED)
|
|
||||||
.orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves scan roots for analysis: the input directory, all Gradle includes when scanning from the
|
|
||||||
* workspace root, and transitively reachable local modules from build-file wiring.
|
|
||||||
*/
|
|
||||||
public Set<Path> resolveScanPaths(Path inputDir) {
|
|
||||||
return resolveScanPaths(inputDir, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> resolveScanPaths(Path inputDir, boolean includeGeneratedSources) {
|
|
||||||
if (inputDir == null) {
|
|
||||||
return Set.of();
|
|
||||||
}
|
|
||||||
Set<Path> scanPaths = new LinkedHashSet<>();
|
|
||||||
Path normalizedInput = inputDir.toAbsolutePath().normalize();
|
|
||||||
scanPaths.add(normalizedInput);
|
|
||||||
|
|
||||||
if (workspaceRoot != null && normalizedInput.equals(workspaceRoot)) {
|
|
||||||
scanPaths.addAll(allModuleDirectories());
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<ProjectModule> inputModule = findContainingModule(normalizedInput);
|
|
||||||
if (inputModule.isPresent()) {
|
|
||||||
scanPaths.addAll(transitiveLocalModuleDirectories(inputModule.get().id()));
|
|
||||||
resolveMavenParentDirectory(inputModule.get().directory()).ifPresent(scanPaths::add);
|
|
||||||
Path parentDir = inputModule.get().directory().getParent();
|
|
||||||
if (parentDir != null && Files.exists(parentDir.resolve("pom.xml"))) {
|
|
||||||
scanPaths.add(parentDir.toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeGeneratedSources) {
|
|
||||||
scanPaths.addAll(new GeneratedSourceDiscovery().discoverSourceRoots(scanPaths));
|
|
||||||
}
|
|
||||||
return scanPaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Optional<Path> resolveMavenParentDirectory(Path moduleDirectory) {
|
|
||||||
if (moduleDirectory == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Path pom = moduleDirectory.resolve("pom.xml");
|
|
||||||
if (!Files.exists(pom)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String content = Files.readString(pom);
|
|
||||||
Matcher parentMatcher = Pattern.compile(
|
|
||||||
"<parent>.*?<relativePath>(.*?)</relativePath>.*?</parent>", Pattern.DOTALL).matcher(content);
|
|
||||||
if (!parentMatcher.find()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
String relPathStr = parentMatcher.group(1).trim();
|
|
||||||
Path relPath = moduleDirectory.resolve(relPathStr).normalize();
|
|
||||||
if (Files.isRegularFile(relPath) && "pom.xml".equals(relPath.getFileName().toString())) {
|
|
||||||
return Optional.of(relPath.getParent().toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
if (Files.isDirectory(relPath)) {
|
|
||||||
return Optional.of(relPath.toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> transitiveLocalModuleDirectories(String rootModuleId) {
|
|
||||||
Set<Path> reachable = new LinkedHashSet<>();
|
|
||||||
Queue<String> queue = new ArrayDeque<>();
|
|
||||||
Set<String> visited = new HashSet<>();
|
|
||||||
if (rootModuleId != null) {
|
|
||||||
queue.add(rootModuleId);
|
|
||||||
}
|
|
||||||
while (!queue.isEmpty()) {
|
|
||||||
String current = queue.poll();
|
|
||||||
if (!visited.add(current)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ProjectModule module = modulesById.get(current);
|
|
||||||
if (module != null && Files.isDirectory(module.directory())) {
|
|
||||||
reachable.add(module.directory().toAbsolutePath().normalize());
|
|
||||||
}
|
|
||||||
for (ModuleDependency dependency : dependencies) {
|
|
||||||
if (dependency.kind() == DependencyKind.LOCAL_PROJECT
|
|
||||||
&& current.equals(dependency.fromModuleId())
|
|
||||||
&& dependency.toModuleId() != null) {
|
|
||||||
queue.add(dependency.toModuleId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ModuleDependency> localDependenciesOf(String moduleId) {
|
|
||||||
List<ModuleDependency> result = new ArrayList<>();
|
|
||||||
for (ModuleDependency dependency : dependencies) {
|
|
||||||
if (moduleId.equals(dependency.fromModuleId())) {
|
|
||||||
result.add(dependency);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.TypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.InfixExpression;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.QualifiedName;
|
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates boolean predicates on rich/domain event types (e.g. {@code myEvent.isCorrect()})
|
|
||||||
* and maps matching implementations to machine enum constants via {@code getType()}.
|
|
||||||
*/
|
|
||||||
public final class RichEventPredicateEvaluator {
|
|
||||||
|
|
||||||
private RichEventPredicateEvaluator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> filterEnumConstants(
|
|
||||||
List<String> candidates,
|
|
||||||
String constraint,
|
|
||||||
String machineEnumFqn,
|
|
||||||
TriggerPoint trigger,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (candidates == null || candidates.isEmpty() || constraint == null || context == null || trigger == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<EnumMemberPredicateEvaluator.PredicateCall> predicates =
|
|
||||||
EnumMemberPredicateEvaluator.extractPredicateCalls(constraint);
|
|
||||||
if (predicates.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
EnumMemberPredicateEvaluator.PredicateCall predicate = predicates.get(0);
|
|
||||||
String receiverTypeFqn = resolveReceiverTypeFqn(trigger, predicate.receiverName(), context);
|
|
||||||
if (receiverTypeFqn == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
Set<String> ownerTypes = new LinkedHashSet<>();
|
|
||||||
ownerTypes.add(receiverTypeFqn);
|
|
||||||
ownerTypes.addAll(context.getImplementations(receiverTypeFqn));
|
|
||||||
|
|
||||||
Set<String> matched = new LinkedHashSet<>();
|
|
||||||
for (String ownerFqn : ownerTypes) {
|
|
||||||
if (!predicateMatches(ownerFqn, predicate, context)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String typeConstant = resolveDirectEnumFromPredicateMethod(ownerFqn, "getType", context);
|
|
||||||
if (typeConstant != null && matchesCandidate(typeConstant, candidates, machineEnumFqn, context)) {
|
|
||||||
matched.add(canonicalize(typeConstant, machineEnumFqn, context));
|
|
||||||
}
|
|
||||||
String directConstant = resolveDirectEnumFromPredicateMethod(ownerFqn, predicate.methodName(), context);
|
|
||||||
if (directConstant != null && matchesCandidate(directConstant, candidates, machineEnumFqn, context)) {
|
|
||||||
matched.add(canonicalize(directConstant, machineEnumFqn, context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<String> filtered = new ArrayList<>();
|
|
||||||
for (String candidate : candidates) {
|
|
||||||
String canonical = canonicalize(candidate, machineEnumFqn, context);
|
|
||||||
if (matched.contains(canonical)) {
|
|
||||||
filtered.add(candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String resolveReceiverTypeFqn(TriggerPoint trigger, String receiverName, CodebaseContext context) {
|
|
||||||
if (trigger.getClassName() == null || trigger.getMethodName() == null || receiverName == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeDeclaration owner = context.getTypeDeclaration(trigger.getClassName());
|
|
||||||
if (owner == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodDeclaration method = context.findMethodDeclaration(owner, trigger.getMethodName(), true);
|
|
||||||
if (method == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
TypeResolver typeResolver = new TypeResolver(context);
|
|
||||||
for (Object parameter : method.parameters()) {
|
|
||||||
if (parameter instanceof org.eclipse.jdt.core.dom.SingleVariableDeclaration variable
|
|
||||||
&& receiverName.equals(variable.getName().getIdentifier())) {
|
|
||||||
return typeResolver.resolveTypeToFqn(variable.getType(), owner.getRoot());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean predicateMatches(
|
|
||||||
String ownerFqn,
|
|
||||||
EnumMemberPredicateEvaluator.PredicateCall predicate,
|
|
||||||
CodebaseContext context) {
|
|
||||||
TypeDeclaration type = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (type == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
MethodDeclaration method = context.findMethodDeclaration(type, predicate.methodName(), true);
|
|
||||||
if (method == null || method.getBody() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Boolean[] result = new Boolean[1];
|
|
||||||
method.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (result[0] == null && node.getExpression() != null) {
|
|
||||||
result[0] = evaluateBoolean(node.getExpression());
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (result[0] == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return predicate.negated() ? !result[0] : result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean evaluateBoolean(Expression expression) {
|
|
||||||
if (expression instanceof org.eclipse.jdt.core.dom.BooleanLiteral literal) {
|
|
||||||
return literal.booleanValue();
|
|
||||||
}
|
|
||||||
if (expression instanceof MethodInvocation invocation
|
|
||||||
&& "isEvent".equals(invocation.getName().getIdentifier())
|
|
||||||
&& invocation.getExpression() instanceof MethodInvocation getType
|
|
||||||
&& "getType".equals(getType.getName().getIdentifier())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (expression instanceof InfixExpression infix && infix.getOperator() == InfixExpression.Operator.EQUALS) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String resolveDirectEnumFromPredicateMethod(
|
|
||||||
String ownerFqn,
|
|
||||||
String predicateMethod,
|
|
||||||
CodebaseContext context) {
|
|
||||||
TypeDeclaration type = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (type == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodDeclaration method = context.findMethodDeclaration(type, predicateMethod, true);
|
|
||||||
if (method == null || method.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final String[] constant = new String[1];
|
|
||||||
method.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (constant[0] != null || node.getExpression() == null) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
constant[0] = extractEnumConstant(node.getExpression());
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return constant[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractEnumConstant(Expression expression) {
|
|
||||||
if (expression instanceof QualifiedName qualifiedName) {
|
|
||||||
return qualifiedName.getFullyQualifiedName();
|
|
||||||
}
|
|
||||||
if (expression instanceof InfixExpression infix && infix.getOperator() == InfixExpression.Operator.EQUALS) {
|
|
||||||
String left = extractEnumConstant(infix.getLeftOperand());
|
|
||||||
if (left != null) {
|
|
||||||
return left;
|
|
||||||
}
|
|
||||||
return extractEnumConstant(infix.getRightOperand());
|
|
||||||
}
|
|
||||||
if (expression instanceof MethodInvocation invocation
|
|
||||||
&& invocation.getExpression() instanceof MethodInvocation getType
|
|
||||||
&& "getType".equals(getType.getName().getIdentifier())) {
|
|
||||||
if (invocation.getExpression() instanceof MethodInvocation inner
|
|
||||||
&& inner.getExpression() instanceof ClassInstanceCreation creation
|
|
||||||
&& !creation.arguments().isEmpty()) {
|
|
||||||
return extractEnumConstant((Expression) creation.arguments().get(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean matchesCandidate(
|
|
||||||
String constant,
|
|
||||||
List<String> candidates,
|
|
||||||
String machineEnumFqn,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (constant == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String canonical = canonicalize(constant, machineEnumFqn, context);
|
|
||||||
for (String candidate : candidates) {
|
|
||||||
if (canonical.equals(canonicalize(candidate, machineEnumFqn, context))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String canonicalize(String constant, String machineEnumFqn, CodebaseContext context) {
|
|
||||||
return MachineEnumCanonicalizer.canonicalizeLabel(constant, machineEnumFqn, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,41 +3,162 @@ package click.kamil.springstatemachineexporter.analysis.resolver;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.*;
|
||||||
import java.util.Set;
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
|
||||||
* Discovers related local modules by statically parsing Maven/Gradle build files (never executes build tools).
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SiblingDependencyResolver {
|
public class SiblingDependencyResolver {
|
||||||
|
|
||||||
private final StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();
|
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 ProjectModuleGraph analyzeProject(Path inputDir) throws IOException {
|
|
||||||
return analyzer.analyze(inputDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Path> resolveSiblings(Path projectDir) {
|
public Set<Path> resolveSiblings(Path projectDir) {
|
||||||
|
Set<Path> siblings = new HashSet<>();
|
||||||
try {
|
try {
|
||||||
ProjectModuleGraph graph = analyzer.analyze(projectDir);
|
siblings.addAll(resolveGradleSiblings(projectDir));
|
||||||
return resolveSiblings(graph, projectDir);
|
siblings.addAll(resolveMavenSiblings(projectDir));
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error resolving sibling dependencies for {}", projectDir, e);
|
log.warn("Error resolving sibling dependencies for {}", projectDir, e);
|
||||||
return Set.of();
|
|
||||||
}
|
}
|
||||||
|
return siblings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<Path> resolveSiblings(ProjectModuleGraph graph, Path inputDir) {
|
private Set<Path> resolveGradleSiblings(Path projectDir) throws IOException {
|
||||||
Set<Path> scanPaths = graph.resolveScanPaths(inputDir);
|
Set<Path> siblings = new HashSet<>();
|
||||||
Set<Path> siblings = new LinkedHashSet<>();
|
Path buildGradle = projectDir.resolve("build.gradle");
|
||||||
Path normalizedInput = inputDir.toAbsolutePath().normalize();
|
if (!Files.exists(buildGradle)) {
|
||||||
for (Path path : scanPaths) {
|
buildGradle = projectDir.resolve("build.gradle.kts");
|
||||||
if (!path.equals(normalizedInput)) {
|
}
|
||||||
siblings.add(path);
|
|
||||||
|
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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.TypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves {@code StateMachineConfigurerAdapter<S, E>} type arguments for a config class.
|
|
||||||
* Used by analysis (canonical enum ids) and routing (machine scoping).
|
|
||||||
*/
|
|
||||||
public final class StateMachineTypeResolver {
|
|
||||||
|
|
||||||
public record MachineTypes(String stateTypeFqn, String eventTypeFqn) {
|
|
||||||
public static MachineTypes empty() {
|
|
||||||
return new MachineTypes(null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private StateMachineTypeResolver() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String[] resolve(String machineConfigFqn, CodebaseContext context) {
|
|
||||||
MachineTypes types = resolveTypes(machineConfigFqn, context);
|
|
||||||
return new String[]{types.stateTypeFqn(), types.eventTypeFqn()};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MachineTypes resolveTypes(String machineConfigFqn, CodebaseContext context) {
|
|
||||||
if (machineConfigFqn == null || context == null) {
|
|
||||||
return MachineTypes.empty();
|
|
||||||
}
|
|
||||||
String configFqn = machineConfigFqn;
|
|
||||||
if (configFqn.contains("#")) {
|
|
||||||
configFqn = configFqn.substring(0, configFqn.indexOf('#'));
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(configFqn);
|
|
||||||
if (td == null) {
|
|
||||||
return MachineTypes.empty();
|
|
||||||
}
|
|
||||||
Set<String> visited = new HashSet<>();
|
|
||||||
visited.add(configFqn);
|
|
||||||
String[] args = findTypeArgumentsRecursively(
|
|
||||||
td.getSuperclassType(),
|
|
||||||
context,
|
|
||||||
(CompilationUnit) td.getRoot(),
|
|
||||||
visited,
|
|
||||||
Map.of());
|
|
||||||
return new MachineTypes(args[0], args[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean extendsEnumStateMachineConfigurer(String machineConfigFqn, CodebaseContext context) {
|
|
||||||
if (machineConfigFqn == null || context == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String configFqn = machineConfigFqn;
|
|
||||||
if (configFqn.contains("#")) {
|
|
||||||
configFqn = configFqn.substring(0, configFqn.indexOf('#'));
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(configFqn);
|
|
||||||
if (td == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Set<String> visited = new HashSet<>();
|
|
||||||
visited.add(configFqn);
|
|
||||||
return hierarchyHasEnumConfigurer(
|
|
||||||
td.getSuperclassType(),
|
|
||||||
context,
|
|
||||||
(CompilationUnit) td.getRoot(),
|
|
||||||
visited);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hierarchyHasEnumConfigurer(
|
|
||||||
Type type,
|
|
||||||
CodebaseContext context,
|
|
||||||
CompilationUnit cu,
|
|
||||||
Set<String> visited) {
|
|
||||||
if (type == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (type instanceof ParameterizedType pt) {
|
|
||||||
String rawTypeName = pt.getType().toString();
|
|
||||||
if (rawTypeName.equals("EnumStateMachineConfigurerAdapter")
|
|
||||||
|| rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.add(fqn)) {
|
|
||||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
|
||||||
if (hierarchyHasEnumConfigurer(superclass, context, cu, visited)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String simpleName = type.toString();
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.add(fqn)) {
|
|
||||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
|
||||||
if (hierarchyHasEnumConfigurer(superclass, context, cu, visited)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String[] findTypeArgumentsRecursively(
|
|
||||||
Type type,
|
|
||||||
CodebaseContext context,
|
|
||||||
CompilationUnit cu,
|
|
||||||
Set<String> visited,
|
|
||||||
Map<String, String> typeVarBindings) {
|
|
||||||
if (type == null) {
|
|
||||||
return new String[]{null, null};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type instanceof ParameterizedType pt) {
|
|
||||||
String rawTypeName = pt.getType().toString();
|
|
||||||
List<?> typeArgs = pt.typeArguments();
|
|
||||||
|
|
||||||
if (isStateMachineConfigurerAdapter(rawTypeName)) {
|
|
||||||
if (typeArgs.size() >= 2) {
|
|
||||||
String stateType = resolveTypeToFqn((Type) typeArgs.get(0), cu, context, typeVarBindings);
|
|
||||||
String eventType = resolveTypeToFqn((Type) typeArgs.get(1), cu, context, typeVarBindings);
|
|
||||||
return new String[]{stateType, eventType};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> nextBindings = extendBindings(rawTypeName, typeArgs, cu, context, typeVarBindings);
|
|
||||||
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.add(fqn)) {
|
|
||||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
|
||||||
String[] res = findTypeArgumentsRecursively(superclass, context, cu, visited, nextBindings);
|
|
||||||
if (res[0] != null || res[1] != null) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String simpleName = type.toString();
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(simpleName, cu);
|
|
||||||
if (td != null) {
|
|
||||||
String fqn = context.getFqn(td);
|
|
||||||
if (visited.add(fqn)) {
|
|
||||||
Type superclass = td instanceof TypeDeclaration typeDecl ? typeDecl.getSuperclassType() : null;
|
|
||||||
String[] res = findTypeArgumentsRecursively(superclass, context, cu, visited, typeVarBindings);
|
|
||||||
if (res[0] != null || res[1] != null) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new String[]{null, null};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> extendBindings(
|
|
||||||
String rawTypeName,
|
|
||||||
List<?> typeArgs,
|
|
||||||
CompilationUnit cu,
|
|
||||||
CodebaseContext context,
|
|
||||||
Map<String, String> parentBindings) {
|
|
||||||
Map<String, String> bindings = new HashMap<>(parentBindings);
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(rawTypeName, cu);
|
|
||||||
if (!(td instanceof TypeDeclaration typeDecl) || typeArgs.isEmpty()) {
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
List<TypeParameter> typeParameters = typeDecl.typeParameters();
|
|
||||||
for (int i = 0; i < Math.min(typeParameters.size(), typeArgs.size()); i++) {
|
|
||||||
String paramName = typeParameters.get(i).getName().getIdentifier();
|
|
||||||
String resolvedArg = resolveTypeToFqn((Type) typeArgs.get(i), cu, context, parentBindings);
|
|
||||||
if (resolvedArg != null) {
|
|
||||||
bindings.put(paramName, resolvedArg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isStateMachineConfigurerAdapter(String rawTypeName) {
|
|
||||||
return rawTypeName.equals("StateMachineConfigurerAdapter")
|
|
||||||
|| rawTypeName.equals("EnumStateMachineConfigurerAdapter")
|
|
||||||
|| rawTypeName.endsWith(".StateMachineConfigurerAdapter")
|
|
||||||
|| rawTypeName.endsWith(".EnumStateMachineConfigurerAdapter");
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveTypeToFqn(Type type, CompilationUnit cu, CodebaseContext context) {
|
|
||||||
return resolveTypeToFqn(type, cu, context, Map.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
static String resolveTypeToFqn(
|
|
||||||
Type type,
|
|
||||||
CompilationUnit cu,
|
|
||||||
CodebaseContext context,
|
|
||||||
Map<String, String> typeVarBindings) {
|
|
||||||
return new TypeResolver(context).resolveTypeToFqn(type, cu, typeVarBindings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
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.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses Maven/Gradle build files statically to discover modules and local dependency wiring.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class StaticBuildFileAnalyzer {
|
|
||||||
|
|
||||||
private static final Pattern GRADLE_PROJECT_DEP = Pattern.compile("project\\(['\"]:(.*?)['\"]\\)");
|
|
||||||
private static final Pattern QUOTED_TOKEN = Pattern.compile("['\"]([^'\"]+)['\"]");
|
|
||||||
private static final Pattern GRADLE_PROJECT_DIR = Pattern.compile(
|
|
||||||
"project\\(['\"]:(.*?)['\"]\\)\\.projectDir\\s*=\\s*(?:file\\(['\"](.*?)['\"]\\)|new File\\(.*?,\\s*['\"](.*?)['\"]\\))");
|
|
||||||
private static final Pattern MAVEN_MODULE = Pattern.compile("<module>(.*?)</module>");
|
|
||||||
private static final Pattern MAVEN_DEPENDENCY_BLOCK = Pattern.compile("<dependency>(.*?)</dependency>", Pattern.DOTALL);
|
|
||||||
private static final Pattern MAVEN_TAG = Pattern.compile("<(groupId|artifactId)>(.*?)</\\1>");
|
|
||||||
|
|
||||||
public ProjectModuleGraph analyze(Path inputDir) throws IOException {
|
|
||||||
Path workspaceRoot = findWorkspaceRoot(inputDir);
|
|
||||||
Path indexRoot = findIndexingRoot(inputDir, workspaceRoot);
|
|
||||||
if (workspaceRoot == null) {
|
|
||||||
workspaceRoot = inputDir.toAbsolutePath().normalize();
|
|
||||||
}
|
|
||||||
if (indexRoot == null) {
|
|
||||||
indexRoot = workspaceRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById = new LinkedHashMap<>();
|
|
||||||
Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates = new LinkedHashMap<>();
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> dependencies = new ArrayList<>();
|
|
||||||
|
|
||||||
Path gradleSettingsRoot = Files.exists(workspaceRoot.resolve("settings.gradle"))
|
|
||||||
|| Files.exists(workspaceRoot.resolve("settings.gradle.kts"))
|
|
||||||
? workspaceRoot
|
|
||||||
: indexRoot;
|
|
||||||
Map<String, Path> gradleModulePaths = parseGradleIncludes(gradleSettingsRoot);
|
|
||||||
for (Map.Entry<String, Path> entry : gradleModulePaths.entrySet()) {
|
|
||||||
registerGradleModule(entry.getKey(), entry.getValue(), modulesById);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Path> pomFiles = findPomFiles(indexRoot);
|
|
||||||
for (Path pom : pomFiles) {
|
|
||||||
registerMavenSubmodules(pom, modulesById, modulesByCoordinates);
|
|
||||||
}
|
|
||||||
for (Path pom : pomFiles) {
|
|
||||||
registerMavenModule(pom, modulesById, modulesByCoordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureInputModule(inputDir, modulesById);
|
|
||||||
|
|
||||||
for (ProjectModuleGraph.ProjectModule module : List.copyOf(modulesById.values())) {
|
|
||||||
if (module.buildSystem() == ProjectModuleGraph.BuildSystem.GRADLE
|
|
||||||
|| module.buildSystem() == ProjectModuleGraph.BuildSystem.MIXED) {
|
|
||||||
dependencies.addAll(parseGradleDependencies(module, modulesById, gradleModulePaths));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Path pom : pomFiles) {
|
|
||||||
dependencies.addAll(parseMavenDependencies(pom, modulesById, modulesByCoordinates));
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Static project graph: {} modules, {} dependencies under {}", modulesById.size(), dependencies.size(), workspaceRoot);
|
|
||||||
return new ProjectModuleGraph(workspaceRoot, modulesById, modulesByCoordinates, dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Path> parseGradleIncludes(Path gradleRoot) throws IOException {
|
|
||||||
Map<String, Path> includes = new LinkedHashMap<>();
|
|
||||||
if (gradleRoot == null) {
|
|
||||||
return includes;
|
|
||||||
}
|
|
||||||
Path settingsFile = gradleRoot.resolve("settings.gradle");
|
|
||||||
if (!Files.exists(settingsFile)) {
|
|
||||||
settingsFile = gradleRoot.resolve("settings.gradle.kts");
|
|
||||||
}
|
|
||||||
if (!Files.exists(settingsFile)) {
|
|
||||||
return includes;
|
|
||||||
}
|
|
||||||
|
|
||||||
String content = Files.readString(settingsFile);
|
|
||||||
for (String includePath : extractGradleIncludePaths(content)) {
|
|
||||||
String normalized = normalizeGradlePath(includePath);
|
|
||||||
Path moduleDir = resolveGradleModuleDirectory(gradleRoot, normalized, content);
|
|
||||||
if (Files.isDirectory(moduleDir)) {
|
|
||||||
includes.put(normalized, moduleDir.normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return includes;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Set<String> extractGradleIncludePaths(String settingsContent) {
|
|
||||||
Set<String> paths = new LinkedHashSet<>();
|
|
||||||
for (String line : settingsContent.split("\\R")) {
|
|
||||||
if (!line.contains("include")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int includeIndex = line.indexOf("include");
|
|
||||||
Matcher tokenMatcher = QUOTED_TOKEN.matcher(line);
|
|
||||||
while (tokenMatcher.find()) {
|
|
||||||
if (tokenMatcher.start() > includeIndex) {
|
|
||||||
paths.add(normalizeGradlePath(tokenMatcher.group(1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeGradlePath(String includePath) {
|
|
||||||
String path = includePath.trim();
|
|
||||||
if (path.startsWith(":")) {
|
|
||||||
path = path.substring(1);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path resolveGradleModuleDirectory(Path gradleRoot, String gradlePath, String settingsContent) {
|
|
||||||
String lookup = gradlePath.contains(":") ? gradlePath : gradlePath.replace('/', ':');
|
|
||||||
Matcher matcher = GRADLE_PROJECT_DIR.matcher(settingsContent);
|
|
||||||
while (matcher.find()) {
|
|
||||||
String configuredPath = matcher.group(1);
|
|
||||||
if (configuredPath != null && configuredPath.equals(lookup)) {
|
|
||||||
String custom = matcher.group(2) != null ? matcher.group(2) : matcher.group(3);
|
|
||||||
if (custom != null) {
|
|
||||||
return gradleRoot.resolve(custom).normalize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gradleRoot.resolve(gradlePath.replace(':', '/')).normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerGradleModule(String gradlePath, Path directory, Map<String, ProjectModuleGraph.ProjectModule> modulesById) {
|
|
||||||
String id = "gradle:" + gradlePath;
|
|
||||||
ProjectModuleGraph.ProjectModule existing = modulesById.get(id);
|
|
||||||
if (existing != null) {
|
|
||||||
modulesById.put(id, new ProjectModuleGraph.ProjectModule(
|
|
||||||
id,
|
|
||||||
directory,
|
|
||||||
ProjectModuleGraph.BuildSystem.MIXED,
|
|
||||||
existing.mavenCoordinates()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
modulesById.put(id, new ProjectModuleGraph.ProjectModule(
|
|
||||||
id,
|
|
||||||
directory,
|
|
||||||
ProjectModuleGraph.BuildSystem.GRADLE,
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerMavenSubmodules(
|
|
||||||
Path pomFile,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
|
|
||||||
String content = Files.readString(pomFile);
|
|
||||||
Path parentDir = pomFile.getParent();
|
|
||||||
Matcher moduleMatcher = MAVEN_MODULE.matcher(content);
|
|
||||||
while (moduleMatcher.find()) {
|
|
||||||
Path moduleDir = parentDir.resolve(moduleMatcher.group(1).trim()).normalize();
|
|
||||||
Path childPom = moduleDir.resolve("pom.xml");
|
|
||||||
if (Files.exists(childPom)) {
|
|
||||||
registerMavenModule(childPom, modulesById, modulesByCoordinates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerMavenModule(
|
|
||||||
Path pomFile,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
|
|
||||||
String content = Files.readString(pomFile);
|
|
||||||
Optional<ProjectModuleGraph.MavenCoordinates> coordinates = extractOwnMavenCoordinates(content);
|
|
||||||
if (coordinates.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Path directory = pomFile.getParent().normalize();
|
|
||||||
|
|
||||||
for (Map.Entry<String, ProjectModuleGraph.ProjectModule> entry : modulesById.entrySet()) {
|
|
||||||
if (entry.getValue().directory().toAbsolutePath().normalize().equals(directory.toAbsolutePath().normalize())) {
|
|
||||||
ProjectModuleGraph.ProjectModule merged = new ProjectModuleGraph.ProjectModule(
|
|
||||||
entry.getKey(),
|
|
||||||
directory,
|
|
||||||
ProjectModuleGraph.BuildSystem.MIXED,
|
|
||||||
coordinates.get());
|
|
||||||
modulesById.put(entry.getKey(), merged);
|
|
||||||
modulesByCoordinates.put(coordinates.get(), merged);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String id = "maven:" + coordinates.get().key();
|
|
||||||
ProjectModuleGraph.ProjectModule module = new ProjectModuleGraph.ProjectModule(
|
|
||||||
id,
|
|
||||||
directory,
|
|
||||||
ProjectModuleGraph.BuildSystem.MAVEN,
|
|
||||||
coordinates.get());
|
|
||||||
modulesById.put(id, module);
|
|
||||||
modulesByCoordinates.put(coordinates.get(), module);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Optional<ProjectModuleGraph.MavenCoordinates> extractOwnMavenCoordinates(String pomContent) {
|
|
||||||
String content = pomContent;
|
|
||||||
int parentEnd = pomContent.indexOf("</parent>");
|
|
||||||
if (parentEnd != -1) {
|
|
||||||
content = pomContent.substring(parentEnd);
|
|
||||||
}
|
|
||||||
String groupId = extractFirstTag(content, "groupId").orElse(null);
|
|
||||||
String artifactId = extractFirstTag(content, "artifactId").orElse(null);
|
|
||||||
if (artifactId == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
if (groupId == null) {
|
|
||||||
groupId = extractParentTag(pomContent, "groupId").orElse("unknown");
|
|
||||||
}
|
|
||||||
return Optional.of(new ProjectModuleGraph.MavenCoordinates(groupId, artifactId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<String> extractParentTag(String pomContent, String tag) {
|
|
||||||
Matcher parentMatcher = Pattern.compile("<parent>.*?</parent>", Pattern.DOTALL).matcher(pomContent);
|
|
||||||
if (!parentMatcher.find()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return extractFirstTag(parentMatcher.group(), tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<String> extractFirstTag(String content, String tag) {
|
|
||||||
Matcher matcher = Pattern.compile("<" + tag + ">(.*?)</" + tag + ">").matcher(content);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return Optional.of(matcher.group(1).trim());
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ProjectModuleGraph.ModuleDependency> parseGradleDependencies(
|
|
||||||
ProjectModuleGraph.ProjectModule module,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<String, Path> gradleModulePaths) throws IOException {
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> result = new ArrayList<>();
|
|
||||||
Path buildFile = module.directory().resolve("build.gradle");
|
|
||||||
if (!Files.exists(buildFile)) {
|
|
||||||
buildFile = module.directory().resolve("build.gradle.kts");
|
|
||||||
}
|
|
||||||
if (!Files.exists(buildFile)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
String content = Files.readString(buildFile);
|
|
||||||
for (String line : content.split("\\R")) {
|
|
||||||
if (isTestGradleConfigurationLine(line)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Matcher matcher = GRADLE_PROJECT_DEP.matcher(line);
|
|
||||||
while (matcher.find()) {
|
|
||||||
String gradlePath = normalizeGradlePath(matcher.group(1));
|
|
||||||
String targetId = findGradleModuleId(gradlePath, modulesById, gradleModulePaths);
|
|
||||||
if (targetId != null) {
|
|
||||||
result.add(new ProjectModuleGraph.ModuleDependency(
|
|
||||||
module.id(), targetId, null, ProjectModuleGraph.DependencyKind.LOCAL_PROJECT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isTestScopedMavenDependency(String dependencyBlock) {
|
|
||||||
return extractFirstTag(dependencyBlock, "scope")
|
|
||||||
.map(scope -> scope.equalsIgnoreCase("test") || scope.equalsIgnoreCase("testCompile"))
|
|
||||||
.orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isTestGradleConfigurationLine(String line) {
|
|
||||||
if (line == null || line.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String trimmed = line.trim();
|
|
||||||
return trimmed.matches("(?i).*(testImplementation|testCompileOnly|testRuntimeOnly|testFixturesImplementation|androidTestImplementation|androidTestCompileOnly|androidTestRuntimeOnly|testAnnotationProcessor)\\b.*");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String findGradleModuleId(
|
|
||||||
String gradlePath,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<String, Path> gradleModulePaths) {
|
|
||||||
String exact = "gradle:" + gradlePath;
|
|
||||||
if (modulesById.containsKey(exact)) {
|
|
||||||
return exact;
|
|
||||||
}
|
|
||||||
Path expectedDir = gradleModulePaths.get(gradlePath);
|
|
||||||
if (expectedDir != null) {
|
|
||||||
for (ProjectModuleGraph.ProjectModule module : modulesById.values()) {
|
|
||||||
if (module.directory().toAbsolutePath().normalize().equals(expectedDir.toAbsolutePath().normalize())) {
|
|
||||||
return module.id();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ProjectModuleGraph.ModuleDependency> parseMavenDependencies(
|
|
||||||
Path pomFile,
|
|
||||||
Map<String, ProjectModuleGraph.ProjectModule> modulesById,
|
|
||||||
Map<ProjectModuleGraph.MavenCoordinates, ProjectModuleGraph.ProjectModule> modulesByCoordinates) throws IOException {
|
|
||||||
List<ProjectModuleGraph.ModuleDependency> result = new ArrayList<>();
|
|
||||||
String content = Files.readString(pomFile);
|
|
||||||
Optional<ProjectModuleGraph.MavenCoordinates> fromCoordinates = extractOwnMavenCoordinates(content);
|
|
||||||
if (fromCoordinates.isEmpty()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
ProjectModuleGraph.ProjectModule fromModule = modulesByCoordinates.get(fromCoordinates.get());
|
|
||||||
if (fromModule == null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int depsStart = content.indexOf("<dependencies>");
|
|
||||||
int depsEnd = content.indexOf("</dependencies>");
|
|
||||||
if (depsStart == -1 || depsEnd <= depsStart) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
String depsSection = content.substring(depsStart, depsEnd);
|
|
||||||
Matcher blockMatcher = MAVEN_DEPENDENCY_BLOCK.matcher(depsSection);
|
|
||||||
while (blockMatcher.find()) {
|
|
||||||
String block = blockMatcher.group(1);
|
|
||||||
if (isTestScopedMavenDependency(block)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String groupId = extractFirstTag(block, "groupId").orElse(null);
|
|
||||||
String artifactId = extractFirstTag(block, "artifactId").orElse(null);
|
|
||||||
if (artifactId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (groupId == null) {
|
|
||||||
groupId = extractParentTag(content, "groupId").orElse("unknown");
|
|
||||||
}
|
|
||||||
ProjectModuleGraph.MavenCoordinates targetCoordinates = new ProjectModuleGraph.MavenCoordinates(groupId, artifactId);
|
|
||||||
ProjectModuleGraph.ProjectModule targetModule = modulesByCoordinates.get(targetCoordinates);
|
|
||||||
if (targetModule != null && !targetModule.id().equals(fromModule.id())) {
|
|
||||||
result.add(new ProjectModuleGraph.ModuleDependency(
|
|
||||||
fromModule.id(),
|
|
||||||
targetModule.id(),
|
|
||||||
null,
|
|
||||||
ProjectModuleGraph.DependencyKind.LOCAL_PROJECT));
|
|
||||||
} else {
|
|
||||||
result.add(new ProjectModuleGraph.ModuleDependency(
|
|
||||||
fromModule.id(),
|
|
||||||
null,
|
|
||||||
targetCoordinates,
|
|
||||||
ProjectModuleGraph.DependencyKind.EXTERNAL_LIBRARY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureInputModule(Path inputDir, Map<String, ProjectModuleGraph.ProjectModule> modulesById) {
|
|
||||||
Path normalized = inputDir.toAbsolutePath().normalize();
|
|
||||||
boolean known = modulesById.values().stream()
|
|
||||||
.anyMatch(module -> module.directory().toAbsolutePath().normalize().equals(normalized));
|
|
||||||
if (!known) {
|
|
||||||
modulesById.put("input:" + normalized.getFileName(), new ProjectModuleGraph.ProjectModule(
|
|
||||||
"input:" + normalized.getFileName(),
|
|
||||||
normalized,
|
|
||||||
ProjectModuleGraph.BuildSystem.MIXED,
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Path> findPomFiles(Path workspaceRoot) throws IOException {
|
|
||||||
if (workspaceRoot == null || !Files.isDirectory(workspaceRoot)) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
try (Stream<Path> stream = Files.walk(workspaceRoot)) {
|
|
||||||
return stream.filter(path -> path.getFileName().toString().equals("pom.xml"))
|
|
||||||
.filter(path -> !path.toString().contains("/target/"))
|
|
||||||
.filter(path -> !path.toString().contains("\\target\\"))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path findWorkspaceRoot(Path start) {
|
|
||||||
if (start == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Path normalized = start.toAbsolutePath().normalize();
|
|
||||||
Path current = normalized;
|
|
||||||
Path deepestMavenAggregator = null;
|
|
||||||
Path deepestGradleSettings = null;
|
|
||||||
Path deepestPom = null;
|
|
||||||
|
|
||||||
while (current != null) {
|
|
||||||
if (Files.exists(current.resolve("settings.gradle")) || Files.exists(current.resolve("settings.gradle.kts"))) {
|
|
||||||
deepestGradleSettings = current;
|
|
||||||
}
|
|
||||||
Path pom = current.resolve("pom.xml");
|
|
||||||
if (Files.exists(pom)) {
|
|
||||||
deepestPom = current;
|
|
||||||
try {
|
|
||||||
if (Files.readString(pom).contains("<modules>")) {
|
|
||||||
deepestMavenAggregator = current;
|
|
||||||
}
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
// continue walking
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deepestMavenAggregator != null) {
|
|
||||||
return deepestMavenAggregator;
|
|
||||||
}
|
|
||||||
if (deepestGradleSettings != null) {
|
|
||||||
return deepestGradleSettings;
|
|
||||||
}
|
|
||||||
if (deepestPom != null) {
|
|
||||||
return deepestPom;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path findIndexingRoot(Path start, Path workspaceRoot) {
|
|
||||||
if (workspaceRoot == null) {
|
|
||||||
return findIndexingRoot(start);
|
|
||||||
}
|
|
||||||
return workspaceRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path findIndexingRoot(Path start) {
|
|
||||||
if (start == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Path current = start.toAbsolutePath().normalize();
|
|
||||||
Path gitRoot = null;
|
|
||||||
while (current != null) {
|
|
||||||
if (Files.exists(current.resolve(".git"))) {
|
|
||||||
gitRoot = current;
|
|
||||||
}
|
|
||||||
current = current.getParent();
|
|
||||||
}
|
|
||||||
return gitRoot != null ? gitRoot : findWorkspaceRoot(start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,167 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
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.resolver.MachineEnumCanonicalizer;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-applies machine-scoped enum canonicalization and validates the analysis model.
|
|
||||||
* Must run after property resolution so placeholders cannot bypass validation.
|
|
||||||
*/
|
|
||||||
public final class AnalysisResultFinalizer {
|
|
||||||
|
|
||||||
private AnalysisResultFinalizer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void finalizeResult(AnalysisResult result, CodebaseContext context) {
|
|
||||||
if (result == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes = JsonExportContextFactory.resolveMachineTypes(
|
|
||||||
result.getName(),
|
|
||||||
result.getStateTypeFqn(),
|
|
||||||
result.getEventTypeFqn(),
|
|
||||||
context);
|
|
||||||
finalizeResult(result, context, machineTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void finalizeResult(
|
|
||||||
AnalysisResult result,
|
|
||||||
CodebaseContext context,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (result == null || machineTypes == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
persistMachineTypes(result, machineTypes);
|
|
||||||
applyCanonicalizationFields(result, context, machineTypes);
|
|
||||||
|
|
||||||
if (context != null) {
|
|
||||||
AnalysisCanonicalFormValidator.enforce(result, context);
|
|
||||||
} else {
|
|
||||||
AnalysisCanonicalFormValidator.enforceWithMachineTypes(result, machineTypes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void persistMachineTypes(
|
|
||||||
AnalysisResult result,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
result.setStateTypeFqn(machineTypes.stateTypeFqn());
|
|
||||||
result.setEventTypeFqn(machineTypes.eventTypeFqn());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Canonicalizes transitions, states, triggers, and matched transitions without validation.
|
|
||||||
* Safe to call before transition relinking when machine types are known.
|
|
||||||
*/
|
|
||||||
public static void applyCanonicalization(
|
|
||||||
AnalysisResult result,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
applyCanonicalization(result, null, machineTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void applyCanonicalization(
|
|
||||||
AnalysisResult result,
|
|
||||||
CodebaseContext context,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (result == null || machineTypes == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (machineTypes.stateTypeFqn() == null && machineTypes.eventTypeFqn() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyCanonicalizationFields(result, context, machineTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void applyCanonicalizationFields(
|
|
||||||
AnalysisResult result,
|
|
||||||
CodebaseContext context,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
MachineEnumCanonicalizer.canonicalizeTransitions(result.getTransitions(), machineTypes, context);
|
|
||||||
|
|
||||||
if (result.getStates() != null) {
|
|
||||||
result.setStates(MachineEnumCanonicalizer.canonicalizeStates(
|
|
||||||
result.getStates(), machineTypes.stateTypeFqn(), context));
|
|
||||||
}
|
|
||||||
result.setStartStates(MachineEnumCanonicalizer.canonicalizeStateLabels(
|
|
||||||
result.getStartStates(), machineTypes.stateTypeFqn(), context));
|
|
||||||
result.setEndStates(MachineEnumCanonicalizer.canonicalizeStateLabels(
|
|
||||||
result.getEndStates(), machineTypes.stateTypeFqn(), context));
|
|
||||||
|
|
||||||
if (result.getMetadata() != null) {
|
|
||||||
CodebaseMetadata metadata = result.getMetadata();
|
|
||||||
List<TriggerPoint> triggers = canonicalizeTriggers(metadata.getTriggers(), context, machineTypes);
|
|
||||||
List<CallChain> callChains = canonicalizeCallChains(metadata.getCallChains(), context, machineTypes);
|
|
||||||
result.setMetadata(CodebaseMetadata.builder()
|
|
||||||
.triggers(triggers)
|
|
||||||
.entryPoints(metadata.getEntryPoints())
|
|
||||||
.callChains(callChains)
|
|
||||||
.properties(metadata.getProperties())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<TriggerPoint> canonicalizeTriggers(
|
|
||||||
List<TriggerPoint> triggers,
|
|
||||||
CodebaseContext context,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (triggers == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return triggers.stream()
|
|
||||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeAndExpandTriggerPoint(
|
|
||||||
trigger, machineTypes, context))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<CallChain> canonicalizeCallChains(
|
|
||||||
List<CallChain> callChains,
|
|
||||||
CodebaseContext context,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
|
||||||
if (callChains == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return callChains.stream()
|
|
||||||
.map(chain -> {
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
|
||||||
? null
|
|
||||||
: MachineEnumCanonicalizer.canonicalizeAndExpandTriggerPoint(
|
|
||||||
chain.getTriggerPoint(), machineTypes, context);
|
|
||||||
List<MatchedTransition> matched = canonicalizeMatchedTransitions(
|
|
||||||
chain.getMatchedTransitions(), machineTypes, context);
|
|
||||||
return chain.toBuilder()
|
|
||||||
.triggerPoint(trigger)
|
|
||||||
.matchedTransitions(matched)
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<MatchedTransition> canonicalizeMatchedTransitions(
|
|
||||||
List<MatchedTransition> matchedTransitions,
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (matchedTransitions == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return matchedTransitions.stream()
|
|
||||||
.map(matched -> MatchedTransition.builder()
|
|
||||||
.event(MachineEnumCanonicalizer.canonicalizeLabel(
|
|
||||||
matched.getEvent(), machineTypes.eventTypeFqn(), context))
|
|
||||||
.sourceState(MachineEnumCanonicalizer.canonicalizeLabel(
|
|
||||||
matched.getSourceState(), machineTypes.stateTypeFqn(), context))
|
|
||||||
.targetState(MachineEnumCanonicalizer.canonicalizeLabel(
|
|
||||||
matched.getTargetState(), machineTypes.stateTypeFqn(), context))
|
|
||||||
.build())
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class CallGraphPathFinder {
|
|
||||||
private final CodebaseContext context;
|
|
||||||
private final Map<java.util.Map.Entry<String, String>, Boolean> heuristicCache = new HashMap<>();
|
|
||||||
|
|
||||||
private final Map<java.util.Map.Entry<String, String>, List<List<String>>> memoCache = new HashMap<>();
|
|
||||||
private final Map<java.util.Map.Entry<String, String>, Boolean> unreachableCache = new HashMap<>();
|
|
||||||
|
|
||||||
public CallGraphPathFinder(CodebaseContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearAnalysisCaches() {
|
|
||||||
heuristicCache.clear();
|
|
||||||
memoCache.clear();
|
|
||||||
unreachableCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
|
||||||
return findAllPaths(start, target, graph, visited, new boolean[]{false});
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<List<String>> findAllPaths(
|
|
||||||
String start,
|
|
||||||
String target,
|
|
||||||
Map<String, List<CallEdge>> graph,
|
|
||||||
Set<String> visited,
|
|
||||||
PathBindingEvaluator bindingEvaluator,
|
|
||||||
Map<String, String> initialBindings) {
|
|
||||||
if (bindingEvaluator == null) {
|
|
||||||
return findAllPaths(start, target, graph, visited);
|
|
||||||
}
|
|
||||||
Map<String, String> bindings = initialBindings == null ? new HashMap<>() : new HashMap<>(initialBindings);
|
|
||||||
return findAllPathsConstrained(
|
|
||||||
start, target, graph, visited, new boolean[]{false}, bindingEvaluator, bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited, boolean[] parentHitCycle) {
|
|
||||||
List<List<String>> result = new ArrayList<>();
|
|
||||||
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(start, target);
|
|
||||||
if (unreachableCache.containsKey(cacheKey)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (memoCache.containsKey(cacheKey)) {
|
|
||||||
List<List<String>> cached = memoCache.get(cacheKey);
|
|
||||||
for (List<String> path : cached) {
|
|
||||||
boolean hasCycle = false;
|
|
||||||
for (String node : path) {
|
|
||||||
if (visited.contains(node)) {
|
|
||||||
hasCycle = true;
|
|
||||||
parentHitCycle[0] = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasCycle) {
|
|
||||||
result.add(new ArrayList<>(path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (start.equals(target)) {
|
|
||||||
result.add(new ArrayList<>(List.of(start)));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (!visited.add(start)) {
|
|
||||||
parentHitCycle[0] = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean[] localHitCycle = new boolean[]{false};
|
|
||||||
|
|
||||||
List<CallEdge> neighbors = graph.get(start);
|
|
||||||
if (neighbors != null) {
|
|
||||||
for (CallEdge edge : neighbors) {
|
|
||||||
String neighbor = edge.getTargetMethod();
|
|
||||||
if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.") || neighbor.startsWith("org.springframework.")) continue;
|
|
||||||
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
|
||||||
List<String> path = new ArrayList<>(List.of(start, target));
|
|
||||||
result.add(path);
|
|
||||||
} else {
|
|
||||||
List<List<String>> subPaths = findAllPaths(neighbor, target, graph, visited, localHitCycle);
|
|
||||||
for (List<String> subPath : subPaths) {
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(subPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle inheritance fallback for concrete class methods not in the graph
|
|
||||||
if (result.isEmpty() && start.contains(".")) {
|
|
||||||
String className = start.substring(0, start.lastIndexOf('.'));
|
|
||||||
String methodName = start.substring(start.lastIndexOf('.') + 1);
|
|
||||||
if (className.contains("<")) {
|
|
||||||
className = className.substring(0, className.indexOf('<'));
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td != null) {
|
|
||||||
String superclass = context.getSuperclassFqn(td);
|
|
||||||
if (superclass != null) {
|
|
||||||
String superMethod = superclass + "." + methodName;
|
|
||||||
if (graph.containsKey(superMethod)) {
|
|
||||||
List<List<String>> superPaths = findAllPaths(superMethod, target, graph, visited, localHitCycle);
|
|
||||||
for (List<String> superPath : superPaths) {
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(superPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle implementation fallback for interfaces/superclasses (interface -> concrete class)
|
|
||||||
if (result.isEmpty() && start.contains(".")) {
|
|
||||||
String className = start.substring(0, start.lastIndexOf('.'));
|
|
||||||
String methodName = start.substring(start.lastIndexOf('.') + 1);
|
|
||||||
if (className.contains("<")) {
|
|
||||||
className = className.substring(0, className.indexOf('<'));
|
|
||||||
}
|
|
||||||
List<String> impls = context.getImplementations(className);
|
|
||||||
if (impls != null) {
|
|
||||||
for (String impl : impls) {
|
|
||||||
String implMethod = impl + "." + methodName;
|
|
||||||
if (graph.containsKey(implMethod)) {
|
|
||||||
List<List<String>> implPaths = findAllPaths(implMethod, target, graph, visited, localHitCycle);
|
|
||||||
for (List<String> implPath : implPaths) {
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(implPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visited.remove(start);
|
|
||||||
|
|
||||||
if (localHitCycle[0]) {
|
|
||||||
parentHitCycle[0] = true;
|
|
||||||
} else if (result.isEmpty()) {
|
|
||||||
unreachableCache.put(cacheKey, true);
|
|
||||||
} else {
|
|
||||||
List<List<String>> toCache = new ArrayList<>();
|
|
||||||
for (List<String> path : result) {
|
|
||||||
toCache.add(new ArrayList<>(path));
|
|
||||||
}
|
|
||||||
memoCache.put(cacheKey, toCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<List<String>> findAllPathsConstrained(
|
|
||||||
String start,
|
|
||||||
String target,
|
|
||||||
Map<String, List<CallEdge>> graph,
|
|
||||||
Set<String> visited,
|
|
||||||
boolean[] parentHitCycle,
|
|
||||||
PathBindingEvaluator bindingEvaluator,
|
|
||||||
Map<String, String> bindings) {
|
|
||||||
List<List<String>> result = new ArrayList<>();
|
|
||||||
if (start.equals(target)) {
|
|
||||||
result.add(new ArrayList<>(List.of(start)));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (!visited.add(start)) {
|
|
||||||
parentHitCycle[0] = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CallEdge> neighbors = graph.get(start);
|
|
||||||
if (neighbors != null) {
|
|
||||||
for (CallEdge edge : neighbors) {
|
|
||||||
String neighbor = edge.getTargetMethod();
|
|
||||||
if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.") || neighbor.startsWith("org.springframework.")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String resolvedTarget = neighbor.equals(target) || isHeuristicMatch(neighbor, target) ? target : neighbor;
|
|
||||||
List<String> hopPath = new ArrayList<>(List.of(start, resolvedTarget));
|
|
||||||
Map<String, String> branchBindings = new HashMap<>(bindings);
|
|
||||||
if (!bindingEvaluator.tryTraverseEdge(
|
|
||||||
start, resolvedTarget, edge, hopPath, 1, graph, this, branchBindings)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedTarget.equals(target)) {
|
|
||||||
result.add(hopPath);
|
|
||||||
} else {
|
|
||||||
List<List<String>> subPaths = findAllPathsConstrained(
|
|
||||||
neighbor, target, graph, visited, parentHitCycle, bindingEvaluator, branchBindings);
|
|
||||||
for (List<String> subPath : subPaths) {
|
|
||||||
if (subPath.isEmpty() || subPath.get(0).equals(start)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(subPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isEmpty() && start.contains(".")) {
|
|
||||||
result.addAll(findInheritanceFallbackPaths(
|
|
||||||
start, target, graph, visited, parentHitCycle, bindingEvaluator, bindings));
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.remove(start);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<List<String>> findInheritanceFallbackPaths(
|
|
||||||
String start,
|
|
||||||
String target,
|
|
||||||
Map<String, List<CallEdge>> graph,
|
|
||||||
Set<String> visited,
|
|
||||||
boolean[] parentHitCycle,
|
|
||||||
PathBindingEvaluator bindingEvaluator,
|
|
||||||
Map<String, String> bindings) {
|
|
||||||
List<List<String>> result = new ArrayList<>();
|
|
||||||
String className = start.substring(0, start.lastIndexOf('.'));
|
|
||||||
String methodName = start.substring(start.lastIndexOf('.') + 1);
|
|
||||||
if (className.contains("<")) {
|
|
||||||
className = className.substring(0, className.indexOf('<'));
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td != null) {
|
|
||||||
String superclass = context.getSuperclassFqn(td);
|
|
||||||
if (superclass != null) {
|
|
||||||
String superMethod = superclass + "." + methodName;
|
|
||||||
if (graph.containsKey(superMethod)) {
|
|
||||||
List<List<String>> superPaths = findAllPathsConstrained(
|
|
||||||
superMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings);
|
|
||||||
for (List<String> superPath : superPaths) {
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(superPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isEmpty()) {
|
|
||||||
List<String> impls = context.getImplementations(className);
|
|
||||||
if (impls != null) {
|
|
||||||
for (String impl : impls) {
|
|
||||||
String implMethod = impl + "." + methodName;
|
|
||||||
if (graph.containsKey(implMethod)) {
|
|
||||||
List<List<String>> implPaths = findAllPathsConstrained(
|
|
||||||
implMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings);
|
|
||||||
for (List<String> implPath : implPaths) {
|
|
||||||
List<String> path = new ArrayList<>();
|
|
||||||
path.add(start);
|
|
||||||
path.addAll(implPath);
|
|
||||||
result.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(neighbor, target);
|
|
||||||
Boolean cached = heuristicCache.get(cacheKey);
|
|
||||||
if (cached != null) return cached;
|
|
||||||
|
|
||||||
boolean result = calculateHeuristicMatch(neighbor, target);
|
|
||||||
heuristicCache.put(cacheKey, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean calculateHeuristicMatch(String neighbor, String target) {
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (context.areSameTypeOrUnambiguousSimpleMatch(classNeighbor, classTarget)) return true;
|
|
||||||
|
|
||||||
if (context.areClassesPolymorphicallyCompatible(classNeighbor, 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;
|
|
||||||
if (!simpleNeighbor.equals(simpleTarget)) return false;
|
|
||||||
|
|
||||||
String classNeighbor = neighbor.contains(".") ? neighbor.substring(0, neighbor.lastIndexOf('.')) : null;
|
|
||||||
String classTarget = target.contains(".") ? target.substring(0, target.lastIndexOf('.')) : null;
|
|
||||||
String simpleClassTarget = classTarget != null && classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
|
|
||||||
String simpleClassNeighbor = classNeighbor != null && classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
|
|
||||||
|
|
||||||
if (simpleClassNeighbor != null && simpleClassTarget != null) {
|
|
||||||
String leftClass = classNeighbor != null ? classNeighbor : simpleClassNeighbor;
|
|
||||||
String rightClass = classTarget != null ? classTarget : simpleClassTarget;
|
|
||||||
if (context.areSameTypeOrUnambiguousSimpleMatch(leftClass, rightClass)) return true;
|
|
||||||
if (!context.isAmbiguousSimpleName(simpleClassNeighbor)
|
|
||||||
&& !context.isAmbiguousSimpleName(simpleClassTarget)
|
|
||||||
&& simpleClassNeighbor.equalsIgnoreCase(simpleClassTarget)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// e.g. "this" vs "com.example.Machine"
|
|
||||||
if (simpleClassNeighbor.equals("this") || simpleClassNeighbor.equals("super")) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have no class ownership information for either side, we can't prove a match beyond the
|
|
||||||
// method name. Fail closed to avoid accidental cross-class linking.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
|
||||||
import org.eclipse.jdt.core.dom.IMethodBinding;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches {@link CallEdge}s to concrete {@link MethodInvocation}s in caller source when
|
|
||||||
* multiple call sites target the same method.
|
|
||||||
*/
|
|
||||||
public final class CallSiteMatcher {
|
|
||||||
|
|
||||||
private static final Pattern ENUM_LITERAL = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*\\.[A-Z_][A-Z0-9_]*");
|
|
||||||
|
|
||||||
private CallSiteMatcher() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CallEdge selectEdge(
|
|
||||||
String caller,
|
|
||||||
String target,
|
|
||||||
List<CallEdge> edges,
|
|
||||||
Map<String, String> bindings) {
|
|
||||||
if (edges == null || edges.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<CallEdge> matching = new ArrayList<>();
|
|
||||||
for (CallEdge edge : edges) {
|
|
||||||
if (edge.getTargetMethod().equals(target) || heuristicTargetMatch(edge.getTargetMethod(), target)) {
|
|
||||||
matching.add(edge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matching.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (matching.size() == 1) {
|
|
||||||
return matching.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CallEdge> constraintFiltered = matching;
|
|
||||||
if (bindings != null && !bindings.isEmpty()) {
|
|
||||||
List<CallEdge> compatible = new ArrayList<>();
|
|
||||||
for (CallEdge edge : matching) {
|
|
||||||
String constraint = edge.getConstraint();
|
|
||||||
if (constraint == null
|
|
||||||
|| constraint.isBlank()
|
|
||||||
|| BooleanConstraintEvaluator.isCompatibleWithBindings(constraint, bindings)) {
|
|
||||||
compatible.add(edge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!compatible.isEmpty()) {
|
|
||||||
constraintFiltered = compatible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (constraintFiltered.size() == 1) {
|
|
||||||
return constraintFiltered.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bindings != null && !bindings.isEmpty()) {
|
|
||||||
for (CallEdge edge : constraintFiltered) {
|
|
||||||
if (edge.getArguments() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (String edgeArg : edge.getArguments()) {
|
|
||||||
for (String bindingValue : bindings.values()) {
|
|
||||||
if (edgeArg == null || bindingValue == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (edgeArg.contains(bindingValue) || bindingValue.contains(edgeArg)) {
|
|
||||||
return edge;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (CallEdge edge : constraintFiltered) {
|
|
||||||
if (findMatchingInvocation(caller, target, edge, null) != null) {
|
|
||||||
return edge;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return constraintFiltered.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MethodInvocation findMatchingInvocation(
|
|
||||||
String caller,
|
|
||||||
String callee,
|
|
||||||
CallEdge edge,
|
|
||||||
CodebaseContext context) {
|
|
||||||
List<MethodInvocation> invocations = findCallInvocations(caller, callee, context);
|
|
||||||
if (invocations.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (edge == null || invocations.size() == 1) {
|
|
||||||
return invocations.get(0);
|
|
||||||
}
|
|
||||||
for (MethodInvocation invocation : invocations) {
|
|
||||||
if (invocationMatchesEdge(invocation, edge)) {
|
|
||||||
return invocation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Expression findCallSiteArgumentExpression(
|
|
||||||
String caller,
|
|
||||||
String callee,
|
|
||||||
int paramIndex,
|
|
||||||
CallEdge edge,
|
|
||||||
CodebaseContext context) {
|
|
||||||
MethodInvocation invocation = findMatchingInvocation(caller, callee, edge, context);
|
|
||||||
if (invocation == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (paramIndex < 0 || paramIndex >= invocation.arguments().size()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (Expression) invocation.arguments().get(paramIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<MethodInvocation> findCallInvocations(String caller, String callee, CodebaseContext context) {
|
|
||||||
if (caller == null || callee == null || !caller.contains(".") || !callee.contains(".") || context == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
String callerClass = caller.substring(0, caller.lastIndexOf('.'));
|
|
||||||
String callerMethod = caller.substring(caller.lastIndexOf('.') + 1);
|
|
||||||
String calleeMethod = callee.substring(callee.lastIndexOf('.') + 1);
|
|
||||||
String calleeClass = callee.substring(0, callee.lastIndexOf('.'));
|
|
||||||
TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
|
|
||||||
if (callerType == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
MethodDeclaration method = context.findMethodDeclaration(callerType, callerMethod, true);
|
|
||||||
if (method == null || method.getBody() == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<MethodInvocation> invocations = new ArrayList<>();
|
|
||||||
method.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (!calleeMethod.equals(node.getName().getIdentifier())) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
IMethodBinding binding = node.resolveMethodBinding();
|
|
||||||
if (binding != null && binding.getDeclaringClass() != null) {
|
|
||||||
String declaringFqn = binding.getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
if (!declaringFqn.equals(calleeClass) && !declaringFqn.endsWith("." + calleeClass)) {
|
|
||||||
String calleeSimple = calleeClass.substring(calleeClass.lastIndexOf('.') + 1);
|
|
||||||
if (!declaringFqn.endsWith("." + calleeSimple)) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
invocations.add(node);
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return invocations;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean invocationMatchesEdge(MethodInvocation invocation, CallEdge edge) {
|
|
||||||
if (edge == null || edge.getArguments() == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
List<?> astArgs = invocation.arguments();
|
|
||||||
List<String> edgeArgs = edge.getArguments();
|
|
||||||
int count = Math.min(astArgs.size(), edgeArgs.size());
|
|
||||||
if (count == 0) {
|
|
||||||
return edgeArgs.isEmpty();
|
|
||||||
}
|
|
||||||
int matched = 0;
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
if (argumentTokensMatch(((Expression) astArgs.get(i)).toString(), edgeArgs.get(i))) {
|
|
||||||
matched++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matched == count;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean argumentTokensMatch(String astArg, String edgeArg) {
|
|
||||||
if (astArg == null || edgeArg == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String left = normalizeArgument(astArg);
|
|
||||||
String right = normalizeArgument(edgeArg);
|
|
||||||
if (left.equals(right)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String leftEnum = extractEnumLiteral(left);
|
|
||||||
String rightEnum = extractEnumLiteral(right);
|
|
||||||
return leftEnum != null && leftEnum.equals(rightEnum);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeArgument(String value) {
|
|
||||||
return value.replaceAll("\\s+", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractEnumLiteral(String value) {
|
|
||||||
var matcher = ENUM_LITERAL.matcher(value);
|
|
||||||
return matcher.find() ? matcher.group() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean heuristicTargetMatch(String edgeTarget, String pathTarget) {
|
|
||||||
if (edgeTarget == null || pathTarget == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (edgeTarget.equals(pathTarget)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String edgeSimple = edgeTarget.substring(edgeTarget.lastIndexOf('.') + 1);
|
|
||||||
String pathSimple = pathTarget.substring(pathTarget.lastIndexOf('.') + 1);
|
|
||||||
return edgeSimple.equals(pathSimple);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,674 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.GetterBodyConstantScanner;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.LibraryUnwrapRegistry;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
|
||||||
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;
|
|
||||||
private ExpressionAccessClassifier expressionAccessClassifier;
|
|
||||||
private AccessorResolver accessorResolver;
|
|
||||||
|
|
||||||
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 setExpressionAccessClassifier(ExpressionAccessClassifier expressionAccessClassifier) {
|
|
||||||
this.expressionAccessClassifier = expressionAccessClassifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAccessorResolver(AccessorResolver accessorResolver) {
|
|
||||||
this.accessorResolver = accessorResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
|
|
||||||
if (constantResolver != null) {
|
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
|
||||||
if (resolved != null && !shouldInspectExpressionStructure(expr, resolved)) {
|
|
||||||
addResolvedConstant(resolved, constants);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
extractConstantsFromSwitchLike(se, constants);
|
|
||||||
} 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) {
|
|
||||||
if (variableTracer != null) {
|
|
||||||
int sizeBefore = constants.size();
|
|
||||||
for (Expression def : variableTracer.traceVariableAll(mi)) {
|
|
||||||
if (def != null && !def.toString().equals(mi.toString())) {
|
|
||||||
extractConstantsFromExpression(def, constants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (constants.size() > sizeBefore) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
|
|
||||||
if (log.isTraceEnabled()) {
|
|
||||||
log.trace("Processing mi: {}", mi);
|
|
||||||
}
|
|
||||||
if (("get".equals(methodName) || "getOrDefault".equals(methodName)) && mi.getExpression() != null) {
|
|
||||||
if (isKeyedLookupMethod(mi)) {
|
|
||||||
extractMapReceiverConstants(mi.getExpression(), constants);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ("get".equals(methodName) && mi.arguments().isEmpty()) {
|
|
||||||
extractMapReceiverConstants(mi.getExpression(), constants);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (LibraryUnwrapRegistry.isUnwrapMethodName(methodName)) {
|
|
||||||
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
|
|
||||||
&& variableTracer != null) {
|
|
||||||
org.eclipse.jdt.core.dom.MethodDeclaration md =
|
|
||||||
click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(mi);
|
|
||||||
TypeDeclaration td = findEnclosingType(mi);
|
|
||||||
if (md != null && td != null) {
|
|
||||||
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
|
||||||
Expression setterArg = variableTracer.traceLocalSetter(methodFqn, sn.getIdentifier(), methodName);
|
|
||||||
if (setterArg != null) {
|
|
||||||
extractConstantsFromExpression(setterArg, constants);
|
|
||||||
if (!constants.isEmpty()) {
|
|
||||||
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 (LibraryUnwrapRegistry.isHeaderSetterMethodName(chainName) && chainMi.arguments().size() == 2) {
|
|
||||||
extractConstantsFromExpression((Expression) chainMi.arguments().get(1), constants);
|
|
||||||
} else if (LibraryUnwrapRegistry.isEventSetterMethodName(chainName) && 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldInspectExpressionStructure(Expression expr, String resolved) {
|
|
||||||
if (expr == null || resolved == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!(resolved.startsWith("<SYMBOLIC:") || resolved.contains("SYMBOLIC:") || resolved.endsWith(".*>"))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return expr instanceof SwitchExpression
|
|
||||||
|| expr instanceof ConditionalExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return resolveMethodReturnConstant(className, methodName, depth, visited, contextCu, ResolutionBudget.defaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> resolveMethodReturnConstant(
|
|
||||||
String className,
|
|
||||||
String methodName,
|
|
||||||
int depth,
|
|
||||||
Set<String> visited,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
ResolutionBudget budget) {
|
|
||||||
return resolveMethodReturnConstant(className, methodName, depth, visited, contextCu, budget, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> resolveMethodReturnConstant(
|
|
||||||
String className,
|
|
||||||
String methodName,
|
|
||||||
int depth,
|
|
||||||
Set<String> visited,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
ResolutionBudget budget,
|
|
||||||
boolean allowWidenToImplementations) {
|
|
||||||
final boolean debug = log.isDebugEnabled();
|
|
||||||
if (debug) {
|
|
||||||
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
|
|
||||||
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
|
||||||
}
|
|
||||||
if (budget == null) {
|
|
||||||
budget = ResolutionBudget.defaults();
|
|
||||||
}
|
|
||||||
final ResolutionBudget activeBudget = budget;
|
|
||||||
if (activeBudget.isMethodReturnExhausted(depth)) {
|
|
||||||
if (debug) {
|
|
||||||
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
String fqn = className + "." + methodName;
|
|
||||||
if (!visited.add(fqn)) {
|
|
||||||
if (debug) {
|
|
||||||
log.debug("Exiting resolveMethodReturnConstant early (cycle detected) for fqn={}", fqn);
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(className, methodName, activeBudget);
|
|
||||||
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
|
||||||
if (tempTd == null) {
|
|
||||||
tempTd = context.getTypeDeclaration(className);
|
|
||||||
}
|
|
||||||
final TypeDeclaration td = tempTd;
|
|
||||||
boolean widenToImplementations = allowWidenToImplementations
|
|
||||||
&& (td == null
|
|
||||||
|| td.isInterface()
|
|
||||||
|| Modifier.isAbstract(td.getModifiers()));
|
|
||||||
|
|
||||||
if (widenToImplementations && depth == 0) {
|
|
||||||
Optional<List<String>> precomputed = context.getPolymorphicAccessorIndex()
|
|
||||||
.widenedReturnConstants(className, methodName);
|
|
||||||
if (precomputed.isPresent()) {
|
|
||||||
visited.remove(fqn);
|
|
||||||
return new ArrayList<>(precomputed.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> constants = new ArrayList<>();
|
|
||||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
|
||||||
boolean indexedOnRequestedType = className.equals(indexedAccessor.get().ownerFqn());
|
|
||||||
if (!widenToImplementations || indexedOnRequestedType) {
|
|
||||||
if (indexedAccessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
|
|
||||||
List<String> constantGetterValues = extractConstantGetterReturn(
|
|
||||||
indexedAccessor.get().ownerFqn(), methodName, contextCu, visited);
|
|
||||||
if (!constantGetterValues.isEmpty()) {
|
|
||||||
if (!widenToImplementations) {
|
|
||||||
visited.remove(fqn);
|
|
||||||
return constantGetterValues;
|
|
||||||
}
|
|
||||||
constants.addAll(constantGetterValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (constants.isEmpty()) {
|
|
||||||
List<String> indexedConstants = resolveIndexedGetterFieldConstants(
|
|
||||||
indexedAccessor.get(), className, contextCu, visited);
|
|
||||||
if (!indexedConstants.isEmpty()) {
|
|
||||||
if (!widenToImplementations) {
|
|
||||||
visited.remove(fqn);
|
|
||||||
return indexedConstants;
|
|
||||||
}
|
|
||||||
constants.addAll(indexedConstants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (td != null) {
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
if (md != null && md.getBody() != null) {
|
|
||||||
for (Object stmtObj : md.getBody().statements()) {
|
|
||||||
if (stmtObj instanceof SwitchStatement switchStatement) {
|
|
||||||
extractConstantsFromSwitchStatement(switchStatement, constants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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, activeBudget, allowWidenToImplementations);
|
|
||||||
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, activeBudget, allowWidenToImplementations);
|
|
||||||
constants.addAll(delegationResult);
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!handled && variableTracer != null && constructorAnalyzer != null) {
|
|
||||||
List<Expression> tracedRetExprs = variableTracer.traceVariableAll(retExpr);
|
|
||||||
if (tracedRetExprs.isEmpty() && retExpr != null) {
|
|
||||||
tracedRetExprs = List.of(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 (val.startsWith("<SYMBOLIC:") || val.contains("SYMBOLIC:") || val.endsWith(".*>")) {
|
|
||||||
if (!constants.contains(val)) {
|
|
||||||
constants.add(val);
|
|
||||||
}
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (widenToImplementations) {
|
|
||||||
List<String> impls = context.getImplementations(className);
|
|
||||||
if (impls != null) {
|
|
||||||
for (String implName : impls) {
|
|
||||||
List<String> delegationResult = resolveMethodReturnConstant(
|
|
||||||
implName, methodName, depth + 1, visited, contextCu, activeBudget, allowWidenToImplementations);
|
|
||||||
for (String value : delegationResult) {
|
|
||||||
if (!constants.contains(value)) {
|
|
||||||
constants.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (widenToImplementations) {
|
|
||||||
List<String> impls = context.getImplementations(className);
|
|
||||||
if (impls != null) {
|
|
||||||
for (String implName : impls) {
|
|
||||||
List<String> delegationResult = resolveMethodReturnConstant(
|
|
||||||
implName, methodName, depth + 1, visited, contextCu, activeBudget);
|
|
||||||
for (String value : delegationResult) {
|
|
||||||
if (!constants.contains(value)) {
|
|
||||||
constants.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visited.remove(fqn);
|
|
||||||
if (debug) {
|
|
||||||
log.debug("resolveMethodReturnConstant className={} methodName={} returns: {}", className, methodName, constants);
|
|
||||||
}
|
|
||||||
return constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> resolveIndexedGetterFieldConstants(
|
|
||||||
AccessorSummary accessor,
|
|
||||||
String requestingTypeFqn,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
Set<String> visited) {
|
|
||||||
if (accessor != null && isSafeForPrecomputedFieldConstants(accessor)) {
|
|
||||||
Optional<List<String>> precomputed = context.getIndexedFieldConstantsIndex()
|
|
||||||
.lookup(accessor.ownerFqn(), accessor.methodName());
|
|
||||||
if (precomputed.isPresent()) {
|
|
||||||
return new ArrayList<>(precomputed.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return AccessorFieldResolver.resolveFieldConstants(
|
|
||||||
accessor,
|
|
||||||
requestingTypeFqn,
|
|
||||||
context,
|
|
||||||
constructorAnalyzer,
|
|
||||||
contextCu,
|
|
||||||
visited,
|
|
||||||
this::extractConstantsFromExpression);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName) {
|
|
||||||
return lookupIndexedGetter(className, methodName, ResolutionBudget.defaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName, ResolutionBudget budget) {
|
|
||||||
if (accessorResolver != null) {
|
|
||||||
return accessorResolver.lookupIndexedGetter(className, methodName);
|
|
||||||
}
|
|
||||||
if (className == null || className.isBlank()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return context.getAccessorIndex().lookup(className, methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> extractConstantGetterReturn(
|
|
||||||
String ownerFqn,
|
|
||||||
String methodName,
|
|
||||||
CompilationUnit contextCu,
|
|
||||||
Set<String> visited) {
|
|
||||||
return GetterBodyConstantScanner.extractConstantGetterReturn(
|
|
||||||
context, this, constantResolver, ownerFqn, methodName, contextCu, visited);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isSafeForPrecomputedFieldConstants(AccessorSummary accessor) {
|
|
||||||
if (accessor.fieldName() == null || accessor.declaringFqn() == null) {
|
|
||||||
return accessor.kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER;
|
|
||||||
}
|
|
||||||
if (!accessor.ownerFqn().equals(accessor.declaringFqn())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
TypeDeclaration ownerType = context.getTypeDeclaration(accessor.ownerFqn());
|
|
||||||
return ownerType != null
|
|
||||||
&& !ownerType.isInterface()
|
|
||||||
&& !Modifier.isAbstract(ownerType.getModifiers());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String parseEnumSetElement(String eVal) {
|
|
||||||
if (eVal != null && (eVal.startsWith("<SYMBOLIC:") || eVal.contains("SYMBOLIC:") || eVal.endsWith(".*>"))) {
|
|
||||||
return eVal;
|
|
||||||
}
|
|
||||||
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void extractConstantsFromSwitchStatement(SwitchStatement switchStatement, List<String> constants) {
|
|
||||||
if (switchStatement == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
extractConstantsFromSwitchLike(switchStatement, constants);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void extractConstantsFromSwitchLike(ASTNode switchNode, List<String> constants) {
|
|
||||||
switchNode.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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isKeyedLookupMethod(MethodInvocation mi) {
|
|
||||||
if (expressionAccessClassifier != null) {
|
|
||||||
return expressionAccessClassifier.isKeyedLookup(mi, null);
|
|
||||||
}
|
|
||||||
return "getOrDefault".equals(mi.getName().getIdentifier())
|
|
||||||
|| ("get".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves constants from the map/collection receiver of {@code .get(key)}.
|
|
||||||
* Uses {@link ConstantResolver} for static fields ({@code ROUTES = Map.of(...)}) first;
|
|
||||||
* falls back to dataflow tracing for locals ({@code Map map = ...; map.get(k)}).
|
|
||||||
*/
|
|
||||||
private void extractMapReceiverConstants(Expression receiver, List<String> constants) {
|
|
||||||
if (tryAddResolvedExpression(receiver, constants)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (receiver instanceof MethodInvocation || receiver instanceof ArrayCreation || receiver instanceof ArrayInitializer) {
|
|
||||||
extractConstantsFromExpression(receiver, constants);
|
|
||||||
if (!constants.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (variableTracer == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String receiverText = receiver.toString();
|
|
||||||
for (Expression traced : variableTracer.traceVariableAll(receiver)) {
|
|
||||||
if (traced == null || receiverText.equals(traced.toString())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (tryAddResolvedExpression(traced, constants)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
extractConstantsFromExpression(traced, constants);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean tryAddResolvedExpression(Expression expr, List<String> constants) {
|
|
||||||
if (constantResolver == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
|
||||||
if (resolved == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
addResolvedConstant(resolved, constants);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addResolvedConstant(String resolved, List<String> constants) {
|
|
||||||
if (resolved.startsWith("ENUM_SET:")) {
|
|
||||||
for (String eVal : resolved.substring(9).split(",")) {
|
|
||||||
String parsed = parseEnumSetElement(eVal);
|
|
||||||
if (!constants.contains(parsed)) {
|
|
||||||
constants.add(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (resolved.startsWith("MAP:")) {
|
|
||||||
List<String> mapValues = ConstantResolver.decodeMapLiteralValues(resolved);
|
|
||||||
if (mapValues != null) {
|
|
||||||
for (String mapValue : mapValues) {
|
|
||||||
if (!constants.contains(mapValue)) {
|
|
||||||
constants.add(mapValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (resolved.startsWith("ARRAY:")) {
|
|
||||||
for (String arrayValue : resolved.substring(6).split("\\|", -1)) {
|
|
||||||
if (!constants.contains(arrayValue)) {
|
|
||||||
constants.add(arrayValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!constants.contains(resolved)) {
|
|
||||||
constants.add(resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
|
||||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,830 +0,0 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class ConstructorAnalyzer {
|
|
||||||
private final CodebaseContext context;
|
|
||||||
private final ConstantResolver constantResolver;
|
|
||||||
private VariableTracer variableTracer;
|
|
||||||
private ConstantExtractor constantExtractor;
|
|
||||||
private final Map<String, ClassInstanceCreation> returnedCicCache = new HashMap<>();
|
|
||||||
private final Set<String> returnedCicAbsent = new HashSet<>();
|
|
||||||
|
|
||||||
@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 void clearReturnedCicCache() {
|
|
||||||
returnedCicCache.clear();
|
|
||||||
returnedCicAbsent.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
|
||||||
return traceFieldInConstructors(td, fieldName, context, visited, ResolutionBudget.defaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> traceFieldInConstructors(
|
|
||||||
TypeDeclaration td,
|
|
||||||
String fieldName,
|
|
||||||
CodebaseContext context,
|
|
||||||
Set<String> visited,
|
|
||||||
ResolutionBudget budget) {
|
|
||||||
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, budget));
|
|
||||||
} 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, budget));
|
|
||||||
} 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, budget.child()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.isEmpty()) {
|
|
||||||
// Context-Aware Fallback (Down): widen only for interface/abstract types
|
|
||||||
if (shouldWidenToImplementations(td)) {
|
|
||||||
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, budget.child()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.isEmpty() && budget != null && budget.allowGlobalFieldScan()) {
|
|
||||||
// Global Fallback: Search all parsed classes in the workspace (explicit opt-in only)
|
|
||||||
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, budget.child()));
|
|
||||||
matchedCount++;
|
|
||||||
if (matchedCount >= budget.globalFieldScanLimit()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.remove(cycleKey);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean shouldWidenToImplementations(TypeDeclaration td) {
|
|
||||||
if (td == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return td.isInterface() || Modifier.isAbstract(td.getModifiers());
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
if (!matches) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Evaluate constructor body regardless of whether parameters could be resolved as constants
|
|
||||||
|
|
||||||
java.util.Map<String, String> localVars = new java.util.HashMap<>(paramValues);
|
|
||||||
constantResolver.evaluateMethodBodyWithLocals(ctor, localVars, context, new java.util.HashSet<>());
|
|
||||||
for (java.util.Map.Entry<String, String> entry : localVars.entrySet()) {
|
|
||||||
if (entry.getKey().startsWith("this.")) {
|
|
||||||
fieldValues.put(entry.getKey(), entry.getValue());
|
|
||||||
fieldValues.put(entry.getKey().substring(5), entry.getValue());
|
|
||||||
} else if (context.hasField(td, entry.getKey())) {
|
|
||||||
fieldValues.put(entry.getKey(), entry.getValue());
|
|
||||||
fieldValues.put("this." + entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 && rhsResolver != null) {
|
|
||||||
TypeDeclaration enclosingTd = findEnclosingType(argExpr);
|
|
||||||
if (enclosingTd != null) {
|
|
||||||
resolved = rhsResolver.resolve(argExpr, subClassParamValues, enclosingTd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (resolved != null) {
|
|
||||||
superParamValues.put(pName, resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (superParamValues.isEmpty()) continue;
|
|
||||||
|
|
||||||
java.util.Map<String, String> localVars = new java.util.HashMap<>(superParamValues);
|
|
||||||
constantResolver.evaluateMethodBodyWithLocals(superCtor, localVars, context, new java.util.HashSet<>());
|
|
||||||
for (java.util.Map.Entry<String, String> entry : localVars.entrySet()) {
|
|
||||||
if (entry.getKey().startsWith("this.")) {
|
|
||||||
fieldValues.put(entry.getKey(), entry.getValue());
|
|
||||||
fieldValues.put(entry.getKey().substring(5), entry.getValue());
|
|
||||||
} else if (context.hasField(superTd, entry.getKey())) {
|
|
||||||
fieldValues.put(entry.getKey(), entry.getValue());
|
|
||||||
fieldValues.put("this." + entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
|
||||||
TypeDeclaration targetTd = context.resolveStaticImport(methodName, cu);
|
|
||||||
if (targetTd != null) {
|
|
||||||
method = context.findMethodDeclaration(targetTd, 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) {
|
|
||||||
String cacheKey = className + "#" + methodName;
|
|
||||||
if (returnedCicAbsent.contains(cacheKey)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (returnedCicCache.containsKey(cacheKey)) {
|
|
||||||
return returnedCicCache.get(cacheKey);
|
|
||||||
}
|
|
||||||
ClassInstanceCreation resolved = findReturnedCIC(className, methodName, context, new HashSet<>(), 0);
|
|
||||||
if (resolved != null) {
|
|
||||||
returnedCicCache.put(cacheKey, resolved);
|
|
||||||
} else {
|
|
||||||
returnedCicAbsent.add(cacheKey);
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClassInstanceCreation findReturnedCIC(
|
|
||||||
String className, String methodName, CodebaseContext context,
|
|
||||||
Set<String> visited, int depth) {
|
|
||||||
if (ResolutionBudget.defaults().isReturnedCicExhausted(depth) || !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, this::evaluateMethodCallInConstructor);
|
|
||||||
if (log.isDebugEnabled()) {
|
|
||||||
log.debug("RESOLVE_GETTER: td={}, fieldValues={}", context.getFqn(td), fieldValues);
|
|
||||||
}
|
|
||||||
if (fieldValues.isEmpty()) return Collections.emptyList();
|
|
||||||
|
|
||||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
|
||||||
if (log.isDebugEnabled()) {
|
|
||||||
log.debug("RESOLVE_GETTER: result={}", result);
|
|
||||||
}
|
|
||||||
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 && arg instanceof MethodInvocation mi) {
|
|
||||||
TypeDeclaration enclosingTd = findEnclosingType(arg);
|
|
||||||
if (enclosingTd != null) {
|
|
||||||
val = evaluateMethodCallInConstructor(arg, new HashMap<>(), enclosingTd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
|
||||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,21 +2,161 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder kept for API stability. The exporter never executes Maven or Gradle against the
|
|
||||||
* analyzed project. Multi-module layout is discovered statically via
|
|
||||||
* {@link click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver}
|
|
||||||
* (parsed {@code settings.gradle}, {@code pom.xml}, {@code build.gradle}).
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class DynamicClasspathResolver {
|
public class DynamicClasspathResolver {
|
||||||
|
|
||||||
public List<String> resolveClasspath(Path projectRoot) {
|
public List<String> resolveClasspath(Path projectRoot) {
|
||||||
log.debug("Skipping external JAR classpath: analysis uses scanned source files only.");
|
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();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user