From 8641c7266fcecde0f0534e97fe326227927ece08 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Fri, 12 Jun 2026 22:59:02 +0200 Subject: [PATCH] enrichment schema --- plan-extended-anaylis-v1/PLAN.md | 35 ++++++ plan-extended-anaylis-v1/architecture.md | 68 ++++++++++++ .../generic_extensibility.md | 92 ++++++++++++++++ .../implementation_details.md | 102 ++++++++++++++++++ .../property_resolution.md | 71 ++++++++++++ plan-extended-anaylis/PLAN.md | 41 +++++++ plan-extended-anaylis/architecture.md | 56 ++++++++++ plan-extended-anaylis/decoupled_logic.md | 40 +++++++ plan-extended-anaylis/enrichment_strategy.md | 67 ++++++++++++ .../generic_extensibility.md | 92 ++++++++++++++++ .../implementation_details.md | 102 ++++++++++++++++++ plan-extended-anaylis/property_resolution.md | 71 ++++++++++++ .../springstatemachineexporter/Main.java | 5 +- .../analysis/enricher/AnalysisEnricher.java | 8 ++ .../analysis/model/AnalysisResult.java | 41 +++++++ .../analysis/model/CodebaseMetadata.java | 28 +++++ .../analysis/service/EnrichmentService.java | 19 ++++ .../service/ExportService.java | 50 +++++++-- .../PlantUmlRenderTest.java | 5 +- .../RegressionTest.java | 5 +- 20 files changed, 986 insertions(+), 12 deletions(-) create mode 100644 plan-extended-anaylis-v1/PLAN.md create mode 100644 plan-extended-anaylis-v1/architecture.md create mode 100644 plan-extended-anaylis-v1/generic_extensibility.md create mode 100644 plan-extended-anaylis-v1/implementation_details.md create mode 100644 plan-extended-anaylis-v1/property_resolution.md create mode 100644 plan-extended-anaylis/PLAN.md create mode 100644 plan-extended-anaylis/architecture.md create mode 100644 plan-extended-anaylis/decoupled_logic.md create mode 100644 plan-extended-anaylis/enrichment_strategy.md create mode 100644 plan-extended-anaylis/generic_extensibility.md create mode 100644 plan-extended-anaylis/implementation_details.md create mode 100644 plan-extended-anaylis/property_resolution.md create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java diff --git a/plan-extended-anaylis-v1/PLAN.md b/plan-extended-anaylis-v1/PLAN.md new file mode 100644 index 0000000..f102831 --- /dev/null +++ b/plan-extended-anaylis-v1/PLAN.md @@ -0,0 +1,35 @@ +# Execution Plan for Extended Analysis + +## Phase 1: Foundation (Current Topic) +- [ ] Define the `TriggerPoint` and `TriggerContext` models. +- [ ] Implement `TriggerDetector` interface. +- [ ] Create `TriggerAggregator` to coordinate multiple detectors. + +## Phase 2: Direct Trigger Detection +- [ ] Implement `GenericEventDetector` using `ASTVisitor` to find `sendEvent` calls. +- [ ] Enhance `CodebaseContext` to provide mapping from `MethodDeclaration` to its callers (within the same codebase). + +## Phase 3: Entry Point Mapping +- [ ] Implement `SpringMvcDetector` for REST endpoints. +- [ ] Implement `MessageListenerDetector` for Kafka/JMS. +- [ ] Implement `ConfigurableTriggerDetector` for user-defined patterns. + +## Phase 4: Call Graph Integration +- [ ] Develop a simple call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls. +- [ ] Support inheritance in call graph resolution (e.g., calling a method defined in an interface/base class). + +## Phase 5: Correlation and Visualization +- [ ] Update `ExportService` to include trigger information. +- [ ] Update DOT exporter to show "external" entry points as boxes pointing to transitions. +- [ ] Update SCXML exporter to include trigger metadata in `` tags. + +## Phase 6: Validation +- [ ] 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 Value Resolution +- [ ] Implement `PropertyResolver` to scan `application.properties/yml`. +- [ ] Add support for `@Value` tracking in fields and constructors. +- [ ] Implement constant resolution for cross-class references in annotations. +- [ ] Integrate value propagation into the call graph analysis (linking injected values to `sendEvent` arguments). diff --git a/plan-extended-anaylis-v1/architecture.md b/plan-extended-anaylis-v1/architecture.md new file mode 100644 index 0000000..3fc99bd --- /dev/null +++ b/plan-extended-anaylis-v1/architecture.md @@ -0,0 +1,68 @@ +# Extended State Machine Analysis: Trigger Point Mapping + +## Overview +The goal of this extension is to bridge the gap between state machine definitions and their real-world usage in application code. By identifying where events are triggered (e.g., REST endpoints, message listeners), we can provide a more holistic view of the system's behavior. + +## Key Concepts +- **Trigger Point**: A location in the source code where an event is sent to a state machine (e.g., `stateMachine.sendEvent(E)`). +- **Entry Point**: A high-level application component (e.g., a REST Controller or Message Listener) that ultimately leads to a Trigger Point. +- **Event Linkage**: The process of connecting an Entry Point to one or more state machine transitions via the event that triggers them. + +## Proposed Architecture + +### 1. TriggerDetector +A generic interface for identifying trigger points and extracting context. +```java +public interface TriggerDetector { + List detect(CompilationUnit cu, CodebaseContext context); +} +``` + +### 2. Predefined Detectors +- **SpringMvcDetector**: Looks for `@RestController` / `@Controller` and mappings like `@PostMapping`. +- **SpringWebFluxDetector**: + - **Annotation-based**: Similar to MVC but handles `Mono`/`Flux` return types. + - **Functional-based**: Scans for `RouterFunction` beans and extracts routes/handlers. +- **MessageListenerDetector**: + - **Kafka**: `@KafkaListener`. + - **RabbitMQ**: `@RabbitListener`. + - **JMS**: `@JmsListener`. +- **GenericEventDetector**: Looks for any `sendEvent` call and tries to find its context. + +### 3. TriggerPoint Model +```java +public record TriggerPoint( + String className, + String methodName, + String event, + TriggerContext context +) {} + +public record TriggerContext( + String type, // "REST", "KAFKA", "JMS", "GENERIC" + Map metadata // path="/api/submit", topic="orders", etc. +) {} +``` + +### 4. Analysis Flow +1. **Scan Phase**: Existing `CodebaseContext.scan(root)`. +2. **Configuration Phase**: Existing `StateMachineAggregator` finds states and transitions. +3. **Trigger Discovery Phase**: New `TriggerAggregator` runs various `TriggerDetector`s over the scanned codebase. +4. **Correlation Phase**: Match `TriggerPoint.event` with `Transition.event`. +5. **Export Phase**: Enhance DOT/SCXML or generate a new "System Flow Map". + +## 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` 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. diff --git a/plan-extended-anaylis-v1/generic_extensibility.md b/plan-extended-anaylis-v1/generic_extensibility.md new file mode 100644 index 0000000..c36b420 --- /dev/null +++ b/plan-extended-anaylis-v1/generic_extensibility.md @@ -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 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 { + @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. diff --git a/plan-extended-anaylis-v1/implementation_details.md b/plan-extended-anaylis-v1/implementation_details.md new file mode 100644 index 0000000..4bcfa4a --- /dev/null +++ b/plan-extended-anaylis-v1/implementation_details.md @@ -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 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 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 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. diff --git a/plan-extended-anaylis-v1/property_resolution.md b/plan-extended-anaylis-v1/property_resolution.md new file mode 100644 index 0000000..d0d123e --- /dev/null +++ b/plan-extended-anaylis-v1/property_resolution.md @@ -0,0 +1,71 @@ +# 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. Property Resolver (The "Config Scanner") +A component dedicated to building a global map of available properties. + +1. **File Scanning**: Parse `application.properties`, `application.yml`, and `bootstrap.yml`. +2. **Profile Support**: Handle `application-{profile}.yml` if a profile is active. +3. **Property Map**: Create a `Map` of all key-value pairs. + +## 4. Value Propagation (Data Flow) +Once a value is resolved or its placeholder is identified, we track its usage. + +**Example Trace**: +1. `application.yml` -> `app.queue: "orders"` +2. `OrderService` -> `@Value("${app.queue}") String q;` -> `this.queue = q;` +3. `OrderService.send()` -> `sm.sendEvent(this.queue);` +4. **Resolution**: The "Event" for this trigger is "orders". + +## 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. diff --git a/plan-extended-anaylis/PLAN.md b/plan-extended-anaylis/PLAN.md new file mode 100644 index 0000000..6eb87ac --- /dev/null +++ b/plan-extended-anaylis/PLAN.md @@ -0,0 +1,41 @@ +# Execution Plan for Extended Analysis + +## Phase 0: Surgical Refactoring (The Enrichment Hook) +- [ ] Create `AnalysisResult` and `CodebaseMetadata` models. +- [ ] Update `ExportService.generateOutputs` to accept `AnalysisResult`. +- [ ] Implement `EnrichmentService` and `AnalysisEnricher` interface. +- [ ] Wrap current parser outputs in `AnalysisResult` within `ExportService`. + +## Phase 1: Foundation +- [ ] Implement `JdtIntelligenceProvider` as the primary semantic analyzer. +- [ ] Implement `RegexIntelligenceProvider` for fast, lightweight metadata extraction. +... + +## Phase 2: Direct Trigger Detection +- [ ] Implement `GenericEventDetector` using `ASTVisitor` to find `sendEvent` calls. +- [ ] Enhance `CodebaseContext` to provide mapping from `MethodDeclaration` to its callers (within the same codebase). + +## Phase 3: Entry Point Mapping +- [ ] Implement `SpringMvcDetector` for REST endpoints. +- [ ] Implement `MessageListenerDetector` for Kafka/JMS. +- [ ] Implement `ConfigurableTriggerDetector` for user-defined patterns. + +## Phase 4: Call Graph Integration +- [ ] Develop a simple call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls. +- [ ] Support inheritance in call graph resolution (e.g., calling a method defined in an interface/base class). + +## Phase 5: Correlation and Visualization +- [ ] Update `ExportService` to include trigger information. +- [ ] Update DOT exporter to show "external" entry points as boxes pointing to transitions. +- [ ] Update SCXML exporter to include trigger metadata in `` tags. + +## Phase 6: Validation +- [ ] 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 Value Resolution +- [ ] Implement `PropertyResolver` to scan `application.properties/yml`. +- [ ] Add support for `@Value` tracking in fields and constructors. +- [ ] Implement constant resolution for cross-class references in annotations. +- [ ] Integrate value propagation into the call graph analysis (linking injected values to `sendEvent` arguments). diff --git a/plan-extended-anaylis/architecture.md b/plan-extended-anaylis/architecture.md new file mode 100644 index 0000000..014ae53 --- /dev/null +++ b/plan-extended-anaylis/architecture.md @@ -0,0 +1,56 @@ +# 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 findTriggerPoints(); + + // Entry Points + List findEntryPoints(); + + // Call Graphs + List 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. + +## 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` 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. diff --git a/plan-extended-anaylis/decoupled_logic.md b/plan-extended-anaylis/decoupled_logic.md new file mode 100644 index 0000000..1ce0ab5 --- /dev/null +++ b/plan-extended-anaylis/decoupled_logic.md @@ -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 triggers, + List entryPoints, + Map 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). diff --git a/plan-extended-anaylis/enrichment_strategy.md b/plan-extended-anaylis/enrichment_strategy.md new file mode 100644 index 0000000..fb471cb --- /dev/null +++ b/plan-extended-anaylis/enrichment_strategy.md @@ -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 transitions; + private final Set startStates; + private final Set endStates; + + // Optional Enhanced Data + private CodebaseMetadata metadata = CodebaseMetadata.empty(); + + public AnalysisResult(String name, List transitions, Set startStates, Set 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. diff --git a/plan-extended-anaylis/generic_extensibility.md b/plan-extended-anaylis/generic_extensibility.md new file mode 100644 index 0000000..c36b420 --- /dev/null +++ b/plan-extended-anaylis/generic_extensibility.md @@ -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 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 { + @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. diff --git a/plan-extended-anaylis/implementation_details.md b/plan-extended-anaylis/implementation_details.md new file mode 100644 index 0000000..4bcfa4a --- /dev/null +++ b/plan-extended-anaylis/implementation_details.md @@ -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 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 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 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. diff --git a/plan-extended-anaylis/property_resolution.md b/plan-extended-anaylis/property_resolution.md new file mode 100644 index 0000000..d0d123e --- /dev/null +++ b/plan-extended-anaylis/property_resolution.md @@ -0,0 +1,71 @@ +# 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. Property Resolver (The "Config Scanner") +A component dedicated to building a global map of available properties. + +1. **File Scanning**: Parse `application.properties`, `application.yml`, and `bootstrap.yml`. +2. **Profile Support**: Handle `application-{profile}.yml` if a profile is active. +3. **Property Map**: Create a `Map` of all key-value pairs. + +## 4. Value Propagation (Data Flow) +Once a value is resolved or its placeholder is identified, we track its usage. + +**Example Trace**: +1. `application.yml` -> `app.queue: "orders"` +2. `OrderService` -> `@Value("${app.queue}") String q;` -> `this.queue = q;` +3. `OrderService.send()` -> `sm.sendEvent(this.queue);` +4. **Resolution**: The "Event" for this trigger is "orders". + +## 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. diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java index 7902a5a..76e464c 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/Main.java @@ -1,5 +1,6 @@ package click.kamil.springstatemachineexporter; +import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService; import click.kamil.springstatemachineexporter.exporter.Dot; import click.kamil.springstatemachineexporter.exporter.JsonExporter; import click.kamil.springstatemachineexporter.exporter.PlantUml; @@ -8,13 +9,15 @@ import click.kamil.springstatemachineexporter.command.ExporterCommand; import click.kamil.springstatemachineexporter.service.ExportService; import picocli.CommandLine; +import java.util.Collections; import java.util.List; public class Main { public static void main(String[] args) { // Manual DI / Wiring var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter()); - var exportService = new ExportService(exporters); + var enrichmentService = new EnrichmentService(Collections.emptyList()); + var exportService = new ExportService(exporters, enrichmentService); var command = new ExporterCommand(exportService); int exitCode = new CommandLine(command).execute(args); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java new file mode 100644 index 0000000..2a6fd21 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/AnalysisEnricher.java @@ -0,0 +1,8 @@ +package click.kamil.springstatemachineexporter.analysis.enricher; + +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; + +public interface AnalysisEnricher { + void enrich(AnalysisResult result, CodebaseContext context); +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java new file mode 100644 index 0000000..6a0533f --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java @@ -0,0 +1,41 @@ +package click.kamil.springstatemachineexporter.analysis.model; + +import click.kamil.springstatemachineexporter.model.Transition; +import lombok.Builder; +import lombok.Getter; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@Getter +@Builder +public class AnalysisResult { + private final String name; + private final List transitions; + private final Set startStates; + private final Set endStates; + private final boolean renderChoicesAsDiamonds; + + @Builder.Default + private CodebaseMetadata metadata = CodebaseMetadata.empty(); + + public void addMetadata(CodebaseMetadata newMetadata) { + if (newMetadata == null) return; + + List combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers()); + combinedTriggers.addAll(newMetadata.getTriggers()); + + List combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints()); + combinedEntryPoints.addAll(newMetadata.getEntryPoints()); + + java.util.Map combinedProperties = new java.util.HashMap<>(this.metadata.getProperties()); + combinedProperties.putAll(newMetadata.getProperties()); + + this.metadata = CodebaseMetadata.builder() + .triggers(Collections.unmodifiableList(combinedTriggers)) + .entryPoints(Collections.unmodifiableList(combinedEntryPoints)) + .properties(Collections.unmodifiableMap(combinedProperties)) + .build(); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java new file mode 100644 index 0000000..08bcabf --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/CodebaseMetadata.java @@ -0,0 +1,28 @@ +package click.kamil.springstatemachineexporter.analysis.model; + +import lombok.Builder; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Getter +@Builder(toBuilder = true) +public class CodebaseMetadata { + private final List triggers; + private final List entryPoints; + private final Map properties; + + public static CodebaseMetadata empty() { + return CodebaseMetadata.builder() + .triggers(Collections.emptyList()) + .entryPoints(Collections.emptyList()) + .properties(Collections.emptyMap()) + .build(); + } + + public record TriggerPoint(String className, String methodName, String event, Map metadata) {} + public record EntryPoint(String className, String methodName, String type, Map metadata) {} +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java new file mode 100644 index 0000000..222feb8 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EnrichmentService.java @@ -0,0 +1,19 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher; +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +public class EnrichmentService { + private final List enrichers; + + public void enrich(AnalysisResult result, CodebaseContext context) { + for (AnalysisEnricher enricher : enrichers) { + enricher.enrich(result, context); + } + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java index 141ecab..84999d3 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java @@ -1,5 +1,7 @@ package click.kamil.springstatemachineexporter.service; +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService; import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser; import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator; import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils; @@ -17,6 +19,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -26,6 +29,7 @@ import java.util.Set; public class ExportService { private final List exporters; + private final EnrichmentService enrichmentService; private static final List TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine"); public static final Set STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory"); @@ -38,7 +42,16 @@ public class ExportService { public void runJsonExporter(Path jsonFile, Path outputDir, List selectedFormats) throws IOException { JsonImportService jsonImportService = new JsonImportService(); StateMachineModel model = jsonImportService.importJson(jsonFile); - generateOutputs(outputDir, model.getName(), model.getTransitions(), model.getStartStates(), model.getEndStates(), selectedFormats, model.isRenderChoicesAsDiamonds()); + + AnalysisResult result = AnalysisResult.builder() + .name(model.getName()) + .transitions(model.getTransitions()) + .startStates(model.getStartStates()) + .endStates(model.getEndStates()) + .renderChoicesAsDiamonds(model.isRenderChoicesAsDiamonds()) + .build(); + + generateOutputs(outputDir, result, selectedFormats); } private void exportAll(CodebaseContext context, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { @@ -84,7 +97,17 @@ public class ExportService { Set startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst); Set endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst); - generateOutputs(outputDir, className, transitions, startStates, endStates, selectedFormats, renderChoicesAsDiamonds); + AnalysisResult result = AnalysisResult.builder() + .name(className) + .transitions(transitions) + .startStates(startStates) + .endStates(endStates) + .renderChoicesAsDiamonds(renderChoicesAsDiamonds) + .build(); + + enrichmentService.enrich(result, context); + + generateOutputs(outputDir, result, selectedFormats); } private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { @@ -100,18 +123,27 @@ public class ExportService { Set startStates = TransitionStateUtils.findStartStates(transitions); Set endStates = TransitionStateUtils.findEndStates(transitions); - generateOutputs(outputDir, uniqueName, transitions, startStates, endStates, selectedFormats, renderChoicesAsDiamonds); + AnalysisResult result = AnalysisResult.builder() + .name(uniqueName) + .transitions(transitions) + .startStates(startStates) + .endStates(endStates) + .renderChoicesAsDiamonds(renderChoicesAsDiamonds) + .build(); + + enrichmentService.enrich(result, context); + + generateOutputs(outputDir, result, selectedFormats); } - private void generateOutputs(Path outputDir, String baseName, List transitions, - Set startStates, Set endStates, List selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { + private void generateOutputs(Path outputDir, AnalysisResult result, List selectedFormats) throws IOException { - Path targetDir = outputDir.resolve(baseName); + Path targetDir = outputDir.resolve(result.getName()); Files.createDirectories(targetDir); ExportOptions options = ExportOptions.builder() .useLambdaGuards(true) - .renderChoicesAsDiamonds(renderChoicesAsDiamonds) + .renderChoicesAsDiamonds(result.isRenderChoicesAsDiamonds()) .build(); for (StateMachineExporter output : exporters) { @@ -120,8 +152,8 @@ public class ExportService { if (!match) continue; } - String content = output.export(baseName, transitions, startStates, endStates, options); - String fileName = baseName + output.getFileExtension(); + String content = output.export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options); + String fileName = result.getName() + output.getFileExtension(); try (PrintWriter out = new PrintWriter(targetDir.resolve(fileName).toFile())) { out.println(content); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlRenderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlRenderTest.java index 70ddde7..3986c30 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlRenderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/PlantUmlRenderTest.java @@ -1,5 +1,6 @@ package click.kamil.springstatemachineexporter; +import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService; import click.kamil.springstatemachineexporter.exporter.PlantUml; import click.kamil.springstatemachineexporter.exporter.StateMachineExporter; import click.kamil.springstatemachineexporter.service.ExportService; @@ -12,6 +13,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -20,7 +22,8 @@ import static org.assertj.core.api.Assertions.assertThat; public class PlantUmlRenderTest { private final List exporters = List.of(new PlantUml()); - private final ExportService exportService = new ExportService(exporters); + private final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList()); + private final ExportService exportService = new ExportService(exporters, enrichmentService); private static List provideTestScenarios() { return List.of( diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java index efc6a82..3510885 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/RegressionTest.java @@ -1,5 +1,6 @@ package click.kamil.springstatemachineexporter; +import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService; import click.kamil.springstatemachineexporter.exporter.Dot; import click.kamil.springstatemachineexporter.exporter.JsonExporter; import click.kamil.springstatemachineexporter.exporter.PlantUml; @@ -13,6 +14,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -20,7 +22,8 @@ import static org.assertj.core.api.Assertions.assertThat; public class RegressionTest { private final List exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter()); - private final ExportService exportService = new ExportService(exporters); + private final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList()); + private final ExportService exportService = new ExportService(exporters, enrichmentService); private static List provideTestScenarios() { return List.of(