diff --git a/plan-extended-anaylis/PLAN.md b/plan-extended-anaylis/PLAN.md deleted file mode 100644 index d9039e0..0000000 --- a/plan-extended-anaylis/PLAN.md +++ /dev/null @@ -1,26 +0,0 @@ - -## Phase 11: Formal Compiler & Static Analysis Alignment (Limitations & Missing Features) -While the custom AST-based `JdtDataFlowModel` supports advanced OOP data flow (like flow-sensitive local mutations, fluent builders, and polymorphic union fallbacks), the following compiler/static analysis behaviors are missing or simplified: - -1. **Full Pointer & Alias Analysis**: - - If two variables alias the same object (e.g., `Command cmd1 = cmd2;`), a mutation on `cmd1` (e.g., `cmd1.setEvent("A")`) is not reflected when reading the fields of `cmd2` unless they resolve to the exact same AST node reference. A formal pointer/alias analysis (like Anderson's or Steensgaard's algorithm) would map abstract heap locations rather than AST nodes. - -2. **Advanced Loop Fixpoints for Fields**: - - If a loop mutates a field based on its previous values (e.g., `while(cond) { builder.count(builder.getCount() + 1); }`), a simple sequential pass will miss the cyclic dependency. A formal data flow framework uses a mathematical lattice and runs a fixpoint iteration algorithm (using meet/join operators) to compute the stable approximation of values. - -3. **Exception Path Analysis (Exception CFG)**: - - Edges for exception flows (`throw` statements, `try-catch-finally` blocks, and implicit unchecked runtime exceptions like `NullPointerException`) are not modeled. Consequently, mutations or return paths within catch/finally blocks may be missed or evaluated out of order. - -4. **Collection & Array Tracking**: - - Arrays and Collections (such as `List`, `Map`, `Set`) are treated as opaque objects, and their operations (like `list.add(val)` or `map.put(key, val)`) are evaluated as general method calls. Modifying a collection does not propagate the values to its getter calls (e.g. `list.get(0)`). - -5. **Field Shadowing and Inheritance Semantics**: - - If a subclass shadows a superclass field (declaring a field with the same name), our resolver might resolve the wrong field binding. Also, implicit superclass constructors and field initializers from superclass hierarchies are not fully simulated. - -6. **Static Initialization Blocks and Static Mutable State**: - - Class loading execution order (i.e. static block initializations `static { ... }`) is not modeled. A mutation on a static field in one method is not propagated to reads in another method. - -7. **Type Erasure & Generics Precision**: - - Generics type bounds (e.g., wildcards `? extends T`, type parameter replacements) are not fully tracked, which can result in incorrect polymorphic resolution when method signatures differ slightly from erased bindings. - - diff --git a/plan-extended-anaylis/advanced_context.md b/plan-extended-anaylis/advanced_context.md deleted file mode 100644 index 7e9fb75..0000000 --- a/plan-extended-anaylis/advanced_context.md +++ /dev/null @@ -1,42 +0,0 @@ -# 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 activeInterceptors -) {} -``` diff --git a/plan-extended-anaylis/architecture.md b/plan-extended-anaylis/architecture.md deleted file mode 100644 index a76a0d7..0000000 --- a/plan-extended-anaylis/architecture.md +++ /dev/null @@ -1,63 +0,0 @@ -# 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. - -## 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` 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 deleted file mode 100644 index 1ce0ab5..0000000 --- a/plan-extended-anaylis/decoupled_logic.md +++ /dev/null @@ -1,40 +0,0 @@ -# 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 deleted file mode 100644 index fb471cb..0000000 --- a/plan-extended-anaylis/enrichment_strategy.md +++ /dev/null @@ -1,67 +0,0 @@ -# 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 deleted file mode 100644 index c36b420..0000000 --- a/plan-extended-anaylis/generic_extensibility.md +++ /dev/null @@ -1,92 +0,0 @@ -# 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 deleted file mode 100644 index 4bcfa4a..0000000 --- a/plan-extended-anaylis/implementation_details.md +++ /dev/null @@ -1,102 +0,0 @@ -# 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/instance_identification.md b/plan-extended-anaylis/instance_identification.md deleted file mode 100644 index 65aae96..0000000 --- a/plan-extended-anaylis/instance_identification.md +++ /dev/null @@ -1,73 +0,0 @@ -# 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` vs `StateMachine`. -- **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`. -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 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. diff --git a/plan-extended-anaylis/polyglot_support.md b/plan-extended-anaylis/polyglot_support.md deleted file mode 100644 index ae73120..0000000 --- a/plan-extended-anaylis/polyglot_support.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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 extractClasses(Path filePath); - List extractMethods(RawClassMetadata cls); - List 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 annotations, - String superclass, - List interfaces -) {} - -public record RawMethodMetadata( - String name, - List annotations, - String returnType, - List 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. diff --git a/plan-extended-anaylis/property_resolution.md b/plan-extended-anaylis/property_resolution.md deleted file mode 100644 index 1985818..0000000 --- a/plan-extended-anaylis/property_resolution.md +++ /dev/null @@ -1,80 +0,0 @@ -# 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>`. - - 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. diff --git a/plan-extended-anaylis/string_resolution.md b/plan-extended-anaylis/string_resolution.md deleted file mode 100644 index b113e84..0000000 --- a/plan-extended-anaylis/string_resolution.md +++ /dev/null @@ -1,53 +0,0 @@ -# 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. diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java index 1897f22..d37c0a1 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java @@ -121,9 +121,15 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { } String expr = constraint; - java.util.List allMachines = java.util.List.of("ORDER", "DOCUMENT", "USER"); - for (String m : allMachines) { - boolean isCurrent = m.equals(cleanMachine); + java.util.regex.Pattern p = java.util.regex.Pattern.compile("[\"']([a-zA-Z0-9_-]+)[\"']"); + java.util.regex.Matcher matcher = p.matcher(constraint); + java.util.Set literals = new java.util.HashSet<>(); + while (matcher.find()) { + literals.add(matcher.group(1)); + } + + for (String m : literals) { + boolean isCurrent = m.equalsIgnoreCase(cleanMachine); String replacement = isCurrent ? "true" : "false"; String regex1 = "(?i)[\"']?" + m + "[\"']?\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\([^)]*\\)"; @@ -191,7 +197,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { private String simplify(String name) { if (name == null) return null; // Strip common suffixes - String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1"); + String simplified = name.replaceAll("(?i)(.+)(Event|Action|Transition|Command)(s)?$", "$1"); if (simplified.isEmpty()) { simplified = name; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java index 1ed1850..2bc8e13 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/HeuristicEventMatchingEngine.java @@ -137,7 +137,7 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine { private String simplify(String name) { if (name == null) return null; - String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1"); + String simplified = name.replaceAll("(?i)(.+)(Event|Action|Transition|Command)(s)?$", "$1"); if (simplified.isEmpty()) { simplified = name; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java index 85b8807..dd566f3 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java @@ -6,6 +6,9 @@ import org.eclipse.jdt.core.dom.*; import java.util.HashSet; import java.util.Set; +import java.util.LinkedHashSet; +import java.util.Arrays; +import java.util.List; @Slf4j public class ConstantResolver { @@ -101,19 +104,34 @@ public class ConstantResolver { if ("valueOf".equals(mi.getName().getIdentifier())) { String enumFqn = null; + Expression arg = null; if (mi.arguments().size() == 2) { enumFqn = resolveEnumFqn((Expression) mi.arguments().get(0), context); + arg = (Expression) mi.arguments().get(1); } else if (mi.arguments().size() == 1 && mi.getExpression() != null) { enumFqn = resolveEnumFqn(mi.getExpression(), context); + arg = (Expression) mi.arguments().get(0); } if (enumFqn != null) { + if (arg != null) { + String resolvedArg = resolveInternal(arg, context, visited); + if (resolvedArg != null && resolvedArg.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { + return enumFqn + "." + resolvedArg; + } + } return ""; } } IMethodBinding mb = mi.resolveMethodBinding(); if (mb != null) { + if (!java.lang.reflect.Modifier.isStatic(mb.getModifiers())) { + Expression receiver = mi.getExpression(); + if (!(receiver instanceof ClassInstanceCreation)) { + return null; + } + } ITypeBinding returnType = mb.getReturnType(); if (returnType != null) { java.util.List values = context.getEnumValues(returnType.getQualifiedName()); @@ -320,7 +338,43 @@ public class ConstantResolver { private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map paramValues, CodebaseContext context, Set visited) { String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues); - if (switchVar == null) return null; + if (switchVar == null) { + Set values = new LinkedHashSet<>(); + for (Object stmtObj : ss.statements()) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs && rs.getExpression() != null) { + String val = resolveInternal(rs.getExpression(), context, visited); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + values.addAll(Arrays.asList(val.substring(9).split(","))); + } else { + values.add(val); + } + } + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys && ys.getExpression() != null) { + String val = resolveInternal(ys.getExpression(), context, visited); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + values.addAll(Arrays.asList(val.substring(9).split(","))); + } else { + values.add(val); + } + } + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es && es.getExpression() != null) { + String val = resolveInternal(es.getExpression(), context, visited); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + values.addAll(Arrays.asList(val.substring(9).split(","))); + } else { + values.add(val); + } + } + } + } + if (!values.isEmpty()) { + return "ENUM_SET:" + String.join(",", values); + } + return null; + } String switchType = null; if (switchVar.contains(".")) { @@ -340,7 +394,7 @@ public class ConstantResolver { if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) { caseVal = caseSn.getIdentifier(); if (switchType != null) { - caseVal = switchType + "." + caseVal; + caseVal = switchType + "." + caseVal; } } if (caseVal != null) { @@ -371,7 +425,43 @@ public class ConstantResolver { private String evaluateSwitchExpression(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map paramValues, CodebaseContext context, Set visited) { String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues); - if (switchVar == null) return null; + if (switchVar == null) { + Set values = new LinkedHashSet<>(); + for (Object stmtObj : se.statements()) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys && ys.getExpression() != null) { + String val = resolveInternal(ys.getExpression(), context, visited); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + values.addAll(Arrays.asList(val.substring(9).split(","))); + } else { + values.add(val); + } + } + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es && es.getExpression() != null) { + String val = resolveInternal(es.getExpression(), context, visited); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + values.addAll(Arrays.asList(val.substring(9).split(","))); + } else { + values.add(val); + } + } + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs && rs.getExpression() != null) { + String val = resolveInternal(rs.getExpression(), context, visited); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + values.addAll(Arrays.asList(val.substring(9).split(","))); + } else { + values.add(val); + } + } + } + } + if (!values.isEmpty()) { + return "ENUM_SET:" + String.join(",", values); + } + return null; + } String switchType = null; if (switchVar.contains(".")) { @@ -657,6 +747,8 @@ public class ConstantResolver { private String resolveEnumFqn(Expression expr, CodebaseContext context) { if (expr == null) return null; + CompilationUnit contextCu = (expr.getRoot() instanceof CompilationUnit) ? (CompilationUnit) expr.getRoot() : null; + if (expr instanceof TypeLiteral tl) { ITypeBinding binding = tl.getType().resolveBinding(); if (binding != null) { @@ -666,8 +758,8 @@ public class ConstantResolver { if (typeName.endsWith(".class")) { typeName = typeName.substring(0, typeName.length() - 6); } - TypeDeclaration td = context.getTypeDeclaration(typeName); - if (td != null) return context.getFqn(td); + String resolved = resolveTypeNameToFqn(typeName, contextCu, context); + if (resolved != null) return resolved; return typeName; } @@ -686,8 +778,47 @@ public class ConstantResolver { if (exprStr.endsWith(".class")) { exprStr = exprStr.substring(0, exprStr.length() - 6); } - TypeDeclaration td = context.getTypeDeclaration(exprStr); - if (td != null) return context.getFqn(td); + String resolved = resolveTypeNameToFqn(exprStr, contextCu, context); + if (resolved != null) return resolved; return exprStr; } + + private String resolveTypeNameToFqn(String name, CompilationUnit contextCu, CodebaseContext context) { + if (name == null || name.isEmpty()) return null; + + if (context.getTypeDeclaration(name) != null || context.getEnumValuesMap().containsKey(name)) { + return name; + } + + if (contextCu != null) { + for (Object typeObj : contextCu.types()) { + if (typeObj instanceof AbstractTypeDeclaration atd) { + if (atd.getName().getIdentifier().equals(name)) { + String pkg = contextCu.getPackage() != null ? contextCu.getPackage().getName().getFullyQualifiedName() : ""; + return pkg.isEmpty() ? name : pkg + "." + name; + } + } + } + + for (Object impObj : contextCu.imports()) { + ImportDeclaration imp = (ImportDeclaration) impObj; + String impName = imp.getName().getFullyQualifiedName(); + if (!imp.isStatic() && !imp.isOnDemand()) { + if (impName.endsWith("." + name)) { + return impName; + } + } + } + + if (contextCu.getPackage() != null) { + String pkg = contextCu.getPackage().getName().getFullyQualifiedName(); + String pkgFqn = pkg + "." + name; + if (context.getEnumValuesMap().containsKey(pkgFqn) || context.getTypeDeclaration(pkgFqn) != null) { + return pkgFqn; + } + } + } + + return null; + } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index acd71e1..ad676e3 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -54,17 +54,20 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { boolean foundAny = false; for (TriggerPoint tp : triggers) { String targetMethod = tp.getClassName() + "." + tp.getMethodName(); - List path = pathFinder.findPath(startMethod, targetMethod, callGraph, new HashSet<>()); - if (path != null) { + List> paths = pathFinder.findAllPaths(startMethod, targetMethod, callGraph, new HashSet<>()); + Set> uniquePaths = new LinkedHashSet<>(paths); + for (List path : uniquePaths) { foundAny = true; TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph); - String contextMachineId = pathFinder.extractContextMachineId(path, callGraph); - chains.add(CallChain.builder() - .entryPoint(ep) - .triggerPoint(resolvedTp) - .methodChain(path) - .contextMachineId(contextMachineId) - .build()); + if (resolvedTp != null) { + String contextMachineId = pathFinder.extractContextMachineId(path, callGraph); + chains.add(CallChain.builder() + .entryPoint(ep) + .triggerPoint(resolvedTp) + .methodChain(path) + .contextMachineId(contextMachineId) + .build()); + } } } if (!foundAny && log.isDebugEnabled()) { @@ -106,10 +109,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } protected TriggerPoint resolveTriggerPointParametersOriginal(TriggerPoint tp, List path, Map> callGraph, String[] finalParamNameRef) { - String event = tp.getEvent(); - String currentParamName = event; - String resolvedValue = event; - String methodSuffix = ""; + click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(path); + try { + String event = tp.getEvent(); + String currentParamName = event; + String resolvedValue = event; + String methodSuffix = ""; String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix); currentParamName = extractedEntry[0]; @@ -121,15 +126,43 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { String caller = path.get(i - 1); int paramIndex = typeResolver.getParameterIndex(target, currentParamName); if (paramIndex < 0) { - Map parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i); - String tracedVar = variableTracer.traceLocalVariable(target, currentParamName, parameterValues); - if (tracedVar != null && !tracedVar.equals(currentParamName)) { - String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix); - tracedVar = extractedTraced[0]; - methodSuffix = extractedTraced[1]; - currentParamName = tracedVar; - resolvedValue = tracedVar + methodSuffix; - paramIndex = typeResolver.getParameterIndex(target, currentParamName); + if ("this".equals(currentParamName)) { + String actualReceiver = null; + for (int j = i; j > 0; j--) { + String t = path.get(j); + String c = path.get(j - 1); + List edges = callGraph.get(c); + if (edges != null) { + for (CallEdge edge : edges) { + if (edge.getTargetMethod().equals(t) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), t)) { + if (edge.getReceiver() != null && !edge.getReceiver().isEmpty()) { + actualReceiver = edge.getReceiver(); + break; + } + } + } + } + if (actualReceiver != null) break; + } + if (actualReceiver != null) { + currentParamName = actualReceiver; + resolvedValue = actualReceiver + methodSuffix; + if (finalParamNameRef != null) finalParamNameRef[0] = currentParamName; + paramIndex = typeResolver.getParameterIndex(target, currentParamName); + } + } + + if (paramIndex < 0) { + Map parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i); + String tracedVar = variableTracer.traceLocalVariable(target, currentParamName, parameterValues); + if (tracedVar != null && !tracedVar.equals(currentParamName)) { + String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix); + tracedVar = extractedTraced[0]; + methodSuffix = extractedTraced[1]; + currentParamName = tracedVar; + resolvedValue = tracedVar + methodSuffix; + paramIndex = typeResolver.getParameterIndex(target, currentParamName); + } } } if (paramIndex < 0) { @@ -230,7 +263,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null - && !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new "); + && (currentParamName.contains("new ") || (!currentParamName.replaceFirst("^this\\.", "").contains(".") && !currentParamName.contains("("))); if (hasGetterOnReceiver) { List getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph); if (getterEvents != null && !getterEvents.isEmpty()) { @@ -629,6 +662,30 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { }); } + List unpacked = new ArrayList<>(); + for (String ev : polymorphicEvents) { + if (ev != null) { + if (ev.startsWith("ENUM_SET:")) { + for (String part : ev.substring(9).split(",")) { + String clean = constantExtractor.parseEnumSetElement(part); + if (clean != null && !unpacked.contains(clean)) { + unpacked.add(clean); + } + } + } else if (ev.startsWith(" 0) { + String className = entryMethod.substring(0, lastDot); + TypeDeclaration entryTd = context.getTypeDeclaration(className); + if (entryTd != null) { + contextCu = (CompilationUnit) entryTd.getRoot(); + } + } + TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType, contextCu); + if (td == null) { + td = context.getTypeDeclaration(simpleReceiverType); + } if (td != null) { Map fieldValues = new HashMap<>(); String varName = receiverName; - Expression initExpr = findVariableInitializer(entryMethod, varName); - if (initExpr instanceof MethodInvocation initMi) { - initExpr = unwrapMethodInvocation(initMi, 0, entryMethod); + ClassInstanceCreation cic = null; + if (directCic != null) { + cic = directCic; + } else { + Expression initExpr = findVariableInitializer(entryMethod, varName); + if (initExpr instanceof MethodInvocation initMi) { + initExpr = unwrapMethodInvocation(initMi, 0, entryMethod); + } + if (initExpr instanceof ClassInstanceCreation) { + cic = (ClassInstanceCreation) initExpr; + } } - if (!(initExpr instanceof ClassInstanceCreation cic)) { + if (cic == null) { return null; } MethodDeclaration getter = null; @@ -883,9 +980,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } if (getter != null && getter.getBody() != null) { fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor)); - int lastDot = entryMethod.lastIndexOf('.'); - if (lastDot > 0) { - String className = entryMethod.substring(0, lastDot); + int lastDotEntry = entryMethod.lastIndexOf('.'); + if (lastDotEntry > 0) { + String className = entryMethod.substring(0, lastDotEntry); String methodName = entryMethod.substring(lastDot + 1); TypeDeclaration entryTd = context.getTypeDeclaration(className); if (entryTd != null) { @@ -962,6 +1059,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { return new String[] { paramName.substring(0, bracketIndex), paramName.substring(bracketIndex) + currentSuffix }; } else if (dotIndex > 0) { if (paramName.startsWith("this.")) { + String afterThis = paramName.substring(5); + if ((afterThis.startsWith("get") || afterThis.startsWith("is") || afterThis.contains("(")) && !afterThis.contains(".")) { + return new String[] { "this", "." + afterThis + currentSuffix }; + } int nextDotIndex = paramName.indexOf('.', 5); if (nextDotIndex > 0) { return new String[] { paramName.substring(0, nextDotIndex), paramName.substring(nextDotIndex) + currentSuffix }; @@ -1113,6 +1214,163 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { return String.join(" && ", constraints); } + protected String resolveConstraint(MethodInvocation node, String calledMethod, String baseConstraint) { + String resolvedConstraint = baseConstraint; + if (node.getExpression() instanceof MethodInvocation miReceiver) { + if (miReceiver.getName().getIdentifier().equals("get") && !miReceiver.arguments().isEmpty() && miReceiver.getExpression() != null) { + Expression keyExpr = (Expression) miReceiver.arguments().get(0); + String mapKeyVar = keyExpr.toString(); + Map keyToType = findMapPopulatedKeys(node, miReceiver.getExpression().toString()); + + int lastDot = calledMethod.lastIndexOf('.'); + if (lastDot > 0) { + String impl = calledMethod.substring(0, lastDot); + for (Map.Entry entry : keyToType.entrySet()) { + String registeredType = entry.getValue(); + if (registeredType.equals(impl) || context.getImplementations(registeredType).contains(impl) || context.getImplementations(impl).contains(registeredType)) { + String mapConstraint = mapKeyVar + " == \"" + entry.getKey() + "\""; + if (resolvedConstraint != null && !resolvedConstraint.isEmpty()) { + resolvedConstraint = resolvedConstraint + " && " + mapConstraint; + } else { + resolvedConstraint = mapConstraint; + } + break; + } + } + } + } + } + return resolvedConstraint; + } + + protected String resolveMapValueType(MethodInvocation mi) { + Expression receiver = mi.getExpression(); + if (receiver == null) return null; + + ITypeBinding binding = receiver.resolveTypeBinding(); + if (binding != null && binding.getTypeArguments().length >= 2) { + ITypeBinding valBinding = binding.getTypeArguments()[1]; + return valBinding.getErasure().getQualifiedName(); + } + + if (receiver instanceof SimpleName sn) { + MethodDeclaration enclosingMethod = findEnclosingMethod(sn); + if (enclosingMethod != null) { + for (Object paramObj : enclosingMethod.parameters()) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj; + if (svd.getName().getIdentifier().equals(sn.getIdentifier())) { + return extractMapValueTypeFromString(svd.getType().toString(), sn); + } + } + } + TypeDeclaration enclosingType = findEnclosingType(sn); + if (enclosingType != null) { + for (FieldDeclaration fd : enclosingType.getFields()) { + for (Object fragObj : fd.fragments()) { + if (fragObj instanceof VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(sn.getIdentifier())) { + return extractMapValueTypeFromString(fd.getType().toString(), sn); + } + } + } + } + } + return null; + } + + private String extractMapValueTypeFromString(String typeStr, ASTNode contextNode) { + if (typeStr.contains("<")) { + String generics = typeStr.substring(typeStr.indexOf('<') + 1, typeStr.lastIndexOf('>')); + String[] parts = generics.split(","); + if (parts.length >= 2) { + String valType = parts[1].trim(); + TypeDeclaration td = context.getTypeDeclaration(valType); + if (td == null) { + CompilationUnit cu = (CompilationUnit) contextNode.getRoot(); + td = context.getTypeDeclaration(valType, cu); + } + if (td != null) { + return context.getFqn(td); + } + return valType; + } + } + return null; + } + + protected Map findMapPopulatedKeys(ASTNode contextNode, String mapVarName) { + Map keyToType = new HashMap<>(); + TypeDeclaration td = findEnclosingType(contextNode); + if (td != null) { + td.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + if (node.getName().getIdentifier().equals("put") && node.getExpression() != null) { + String exprStr = node.getExpression().toString(); + if (exprStr.equals(mapVarName) || exprStr.endsWith("." + mapVarName)) { + if (node.arguments().size() >= 2) { + Expression keyArg = (Expression) node.arguments().get(0); + Expression valArg = (Expression) node.arguments().get(1); + + String keyVal = constantResolver.resolve(keyArg, context); + if (keyVal == null) { + keyVal = keyArg.toString().replaceAll("[\"']", ""); + } + + String valType = null; + ITypeBinding valBinding = valArg.resolveTypeBinding(); + if (valBinding != null) { + valType = valBinding.getErasure().getQualifiedName(); + } + if (valType == null && valArg instanceof SimpleName sn) { + MethodDeclaration enclosingMethod = findEnclosingMethod(sn); + if (enclosingMethod != null) { + for (Object paramObj : enclosingMethod.parameters()) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj; + if (svd.getName().getIdentifier().equals(sn.getIdentifier())) { + if (svd.getType().isSimpleType()) { + valType = ((SimpleType) svd.getType()).getName().getFullyQualifiedName(); + } else { + valType = svd.getType().toString(); + } + } + } + } + TypeDeclaration enclosingType = findEnclosingType(sn); + if (enclosingType != null) { + for (FieldDeclaration fd : enclosingType.getFields()) { + for (Object fragObj : fd.fragments()) { + if (fragObj instanceof VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(sn.getIdentifier())) { + if (fd.getType().isSimpleType()) { + valType = ((SimpleType) fd.getType()).getName().getFullyQualifiedName(); + } else { + valType = fd.getType().toString(); + } + } + } + } + } + } + if (valType != null && keyVal != null) { + TypeDeclaration valTd = context.getTypeDeclaration(valType); + if (valTd == null) { + CompilationUnit cu = (CompilationUnit) node.getRoot(); + valTd = context.getTypeDeclaration(valType, cu); + } + if (valTd != null) { + valType = context.getFqn(valTd); + } + keyToType.put(keyVal, valType); + } + } + } + } + return super.visit(node); + } + }); + } + return keyToType; + } + protected abstract Map> buildCallGraph(); protected abstract String resolveCalledMethod(MethodInvocation node); } \ No newline at end of file diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java index 0633b04..b4ffa8b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java @@ -2,6 +2,7 @@ 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.*; @@ -47,6 +48,64 @@ public class CallGraphPathFinder { return null; } + public List> findAllPaths(String start, String target, Map> graph, Set visited) { + List> result = new ArrayList<>(); + if (start.equals(target)) { + result.add(new ArrayList<>(List.of(start))); + return result; + } + if (!visited.add(start)) { + return result; + } + + List neighbors = graph.get(start); + if (neighbors != null) { + for (CallEdge edge : neighbors) { + String neighbor = edge.getTargetMethod(); + if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) { + List path = new ArrayList<>(List.of(start, target)); + result.add(path); + } else { + List> subPaths = findAllPaths(neighbor, target, graph, visited); + for (List subPath : subPaths) { + List 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> superPaths = findAllPaths(superMethod, target, graph, visited); + for (List superPath : superPaths) { + List path = new ArrayList<>(); + path.add(start); + path.addAll(superPath); + result.add(path); + } + } + } + } + } + + visited.remove(start); + return result; + } + public String extractContextMachineId(List path, Map> callGraph) { for (String node : path) { List edges = callGraph.get(node); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java index a23eb72..2891adf 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java @@ -341,6 +341,9 @@ public class ConstantExtractor { } if (!handled && variableTracer != null && constructorAnalyzer != null) { List 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); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java index 71543e7..d724027 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java @@ -245,7 +245,8 @@ public class ConstructorAnalyzer { boolean matches = false; if (resolvedCtor != null && ctor.resolveBinding() != null) { matches = resolvedCtor.isEqualTo(ctor.resolveBinding()) || resolvedCtor.getKey().equals(ctor.resolveBinding().getKey()); - } else { + } + if (!matches) { matches = ctor.parameters().size() == cic.arguments().size(); } if (!matches) continue; @@ -271,7 +272,7 @@ public class ConstructorAnalyzer { if (resolved != null) paramValues.put(pName, resolved); } } - if (paramValues.isEmpty()) continue; + // Evaluate constructor body regardless of whether parameters could be resolved as constants ctor.getBody().accept(new ASTVisitor() { @Override @@ -383,6 +384,12 @@ public class ConstructorAnalyzer { 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); } @@ -472,6 +479,13 @@ public class ConstructorAnalyzer { } 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; } @@ -576,10 +590,12 @@ public class ConstructorAnalyzer { } try { - Map fieldValues = buildFieldValuesFromCIC(td, cic, context); + Map fieldValues = buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor); + System.out.println("RESOLVE_GETTER: td=" + context.getFqn(td) + ", fieldValues=" + fieldValues); if (fieldValues.isEmpty()) return Collections.emptyList(); String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited); + System.out.println("RESOLVE_GETTER: result=" + result); return result != null ? List.of(result) : Collections.emptyList(); } finally { visited.remove(getterKey); @@ -665,6 +681,12 @@ public class ConstructorAnalyzer { 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(",")) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index a0ef4bc..57b0c57 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -38,7 +38,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { for (String calledMethod : calledMethods) { String receiver = getReceiverString(node.getExpression()); CallEdge edge = new CallEdge(calledMethod, args, receiver); - edge.setConstraint(constraint); + edge.setConstraint(resolveConstraint(node, calledMethod, constraint)); graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge); } for (Object argObj : node.arguments()) { @@ -166,22 +166,29 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { if (baseCalled == null) return Collections.emptyList(); List allResolved = new ArrayList<>(); - allResolved.add(baseCalled); if (baseCalled.contains(".")) { String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); - String definingClass = findDefiningClass(className, methodName); - if (definingClass != null && !definingClass.equals(className)) { - allResolved.set(0, definingClass + "." + methodName); - className = definingClass; - } - List impls = context.getImplementations(className); - for (String impl : impls) { - allResolved.add(impl + "." + methodName); + if (impls.isEmpty()) { + allResolved.add(className + "." + methodName); + } else { + TypeDeclaration td = context.getTypeDeclaration(className); + boolean isAbstractOrInterface = false; + if (td != null) { + isAbstractOrInterface = td.isInterface() || (td.modifiers() != null && td.modifiers().toString().contains("abstract")); + } + if (!isAbstractOrInterface) { + allResolved.add(className + "." + methodName); + } + for (String impl : impls) { + allResolved.add(impl + "." + methodName); + } } + } else { + allResolved.add(baseCalled); } return allResolved; @@ -619,6 +626,14 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { } if (receiverType != null) { + String rawType = receiverType; + if (rawType.contains("<")) { + rawType = rawType.substring(0, rawType.indexOf('<')); + } + if (rawType.equals("java.util.Map") || rawType.equals("Map")) { + String valType = resolveMapValueType(mi); + if (valType != null) return valType; + } if (receiverType.contains("<")) { receiverType = receiverType.substring(0, receiverType.indexOf('<')); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index 3fa1755..0957553 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -41,7 +41,7 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node); for (String calledMethod : calledMethods) { CallEdge edge = new CallEdge(calledMethod, args); - edge.setConstraint(constraint); + edge.setConstraint(resolveConstraint(node, calledMethod, constraint)); graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge); } for (Object argObj : node.arguments()) { @@ -163,16 +163,29 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { if (baseCalled == null) return Collections.emptyList(); List allResolved = new ArrayList<>(); - allResolved.add(baseCalled); if (baseCalled.contains(".")) { String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); List impls = context.getImplementations(className); - for (String impl : impls) { - allResolved.add(impl + "." + methodName); + if (impls.isEmpty()) { + allResolved.add(baseCalled); + } else { + TypeDeclaration td = context.getTypeDeclaration(className); + boolean isAbstractOrInterface = false; + if (td != null) { + isAbstractOrInterface = td.isInterface() || (td.modifiers() != null && td.modifiers().toString().contains("abstract")); + } + if (!isAbstractOrInterface) { + allResolved.add(baseCalled); + } + for (String impl : impls) { + allResolved.add(impl + "." + methodName); + } } + } else { + allResolved.add(baseCalled); } return allResolved; @@ -391,6 +404,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { } if (receiverType != null) { + String rawType = receiverType; + if (rawType.contains("<")) { + rawType = rawType.substring(0, rawType.indexOf('<')); + } + if (rawType.equals("java.util.Map") || rawType.equals("Map")) { + String valType = resolveMapValueType(mi); + if (valType != null) return valType; + } if (receiverType.contains("<")) { receiverType = receiverType.substring(0, receiverType.indexOf('<')); } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index 04ace62..aa3b9fb 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -285,17 +285,25 @@ public class CodebaseContext { } public List getImplementations(String interfaceName) { + String cleanName = interfaceName; + if (interfaceName != null && interfaceName.contains("<")) { + cleanName = interfaceName.substring(0, interfaceName.indexOf('<')); + } Set allImpls = new HashSet<>(); - collectImplementations(interfaceName, allImpls, new HashSet<>()); + collectImplementations(cleanName, allImpls, new HashSet<>()); System.out.println("GET IMPLEMENTATIONS FOR: " + interfaceName + " -> " + allImpls); return new ArrayList<>(allImpls); } private void collectImplementations(String typeName, Set results, Set visited) { - if (!visited.add(typeName)) return; + String cleanName = typeName; + if (typeName != null && typeName.contains("<")) { + cleanName = typeName.substring(0, typeName.indexOf('<')); + } + if (!visited.add(cleanName)) return; // Try direct match - List directImpls = interfaceToImpls.get(typeName); + List directImpls = interfaceToImpls.get(cleanName); // Try FQN match if input was simple name if (directImpls == null) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java index 558f8ce..22d2975 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/JdtDataFlowModel.java @@ -9,6 +9,16 @@ public class JdtDataFlowModel implements DataFlowModel { private final Map rdCache = new HashMap<>(); private final Map> objectFieldBindings = new HashMap<>(); + private static final ThreadLocal> CURRENT_PATH = new ThreadLocal<>(); + + public static void setCurrentPath(List path) { + CURRENT_PATH.set(path); + } + + public static void clearCurrentPath() { + CURRENT_PATH.remove(); + } + public JdtDataFlowModel(CodebaseContext context) { this.context = context; } @@ -82,14 +92,16 @@ public class JdtDataFlowModel implements DataFlowModel { } } - // 3. Handle Parameter mapping for SimpleName + // 3. Handle Parameter Variable Bindings (Propagate values through parameters) if (expr instanceof SimpleName sn) { IBinding b = sn.resolveBinding(); - if (b instanceof IVariableBinding paramVb && paramBindings.containsKey(paramVb)) { - Expression argExpr = paramBindings.get(paramVb); - List resolved = getReachingDefinitions(argExpr, visited, paramBindings, instanceFieldBindings, depth + 1); - visited.remove(expr); - return resolved; + if (b instanceof IVariableBinding paramVb) { + if (paramBindings.containsKey(paramVb)) { + Expression argExpr = paramBindings.get(paramVb); + List resolved = getReachingDefinitions(argExpr, visited, paramBindings, instanceFieldBindings, depth + 1); + visited.remove(expr); + return resolved; + } } // Fallback: Local Reaching Definitions @@ -134,6 +146,44 @@ public class JdtDataFlowModel implements DataFlowModel { return resolved; } + // Handle SwitchExpression + if (expr instanceof org.eclipse.jdt.core.dom.SwitchExpression se) { + List results = new ArrayList<>(); + List selectorDefs = getReachingDefinitions(se.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1); + String selectorVal = null; + if (!selectorDefs.isEmpty()) { + selectorVal = resolveValue(selectorDefs.get(0), context); + } + + boolean active = (selectorVal == null); + for (Object stmtObj : se.statements()) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) { + if (selectorVal != null) { + active = false; + for (Object expObj : sc.expressions()) { + Expression caseExpr = (Expression) expObj; + String caseVal = resolveValue(caseExpr, context); + if (caseVal != null && (caseVal.equals(selectorVal) || selectorVal.endsWith("." + caseVal) || caseVal.endsWith("." + selectorVal))) { + active = true; + break; + } + } + if (sc.isDefault()) { + active = true; + } + } + } else if (active) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys && ys.getExpression() != null) { + results.addAll(getReachingDefinitions(ys.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1)); + } else if (stmtObj instanceof ExpressionStatement es) { + results.addAll(getReachingDefinitions(es.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1)); + } + } + } + visited.remove(expr); + return results; + } + // 4. Handle Method Invocations (Static Factory methods / Transforming Getters) if (expr instanceof MethodInvocation mi) { String mName = mi.getName().getIdentifier(); @@ -374,7 +424,7 @@ public class JdtDataFlowModel implements DataFlowModel { MethodDeclaration cd = findMethodDeclaration(cb); if (cd == null || cd.getBody() == null) return; - Map currentParamValues = new HashMap<>(); + Map currentParamValues = new HashMap<>(callerParamValues); List parameters = cd.parameters(); int count = Math.min(parameters.size(), arguments.size()); for (int i = 0; i < count; i++) { @@ -386,6 +436,7 @@ public class JdtDataFlowModel implements DataFlowModel { currentParamValues.put(paramVb, resolvedArg); } } + System.out.println("EVAL_CTOR: cd=" + cd.getName() + ", currentParamValues=" + currentParamValues); List statements = cd.getBody().statements(); if (!statements.isEmpty()) { @@ -425,7 +476,12 @@ public class JdtDataFlowModel implements DataFlowModel { if (fieldVb != null) { Expression resolvedRhs = resolveParamValue(rhs, currentParamValues); - fieldBindings.put(fieldVb, resolvedRhs); + List evaluated = getReachingDefinitions(resolvedRhs, new HashSet<>(), currentParamValues, fieldBindings, 0); + if (!evaluated.isEmpty()) { + fieldBindings.put(fieldVb, evaluated.get(0)); + } else { + fieldBindings.put(fieldVb, resolvedRhs); + } } return super.visit(assignment); } @@ -533,6 +589,19 @@ public class JdtDataFlowModel implements DataFlowModel { if (declaringClassFqn != null && !declaringClassFqn.isEmpty()) { // Find all known implementation subclasses in the codebase List implClassFqns = context.getImplementations(declaringClassFqn); + List currentPath = CURRENT_PATH.get(); + if (currentPath != null && implClassFqns != null && !implClassFqns.isEmpty()) { + Set contextTypes = getCompatibleContextTypes(currentPath, declaringClassFqn); + List filteredImpls = new ArrayList<>(); + for (String implFqn : implClassFqns) { + if (contextTypes.contains(implFqn)) { + filteredImpls.add(implFqn); + } + } + if (!filteredImpls.isEmpty()) { + implClassFqns = filteredImpls; + } + } if (implClassFqns != null && !implClassFqns.isEmpty()) { for (String implFqn : implClassFqns) { TypeDeclaration td = context.getTypeDeclaration(implFqn); @@ -895,4 +964,74 @@ public class JdtDataFlowModel implements DataFlowModel { } return firstVal != null ? firstVal : context.resolveExpression(expr); } + + private Set getCompatibleContextTypes(List currentPath, String declaringClassFqn) { + Set contextTypes = new HashSet<>(); + List implementations = context.getImplementations(declaringClassFqn); + + // 1. Direct class name from the path itself (prioritize concrete implementations in the path) + for (String methodFqn : currentPath) { + int lastDot = methodFqn.lastIndexOf('.'); + if (lastDot > 0) { + String pathClass = methodFqn.substring(0, lastDot); + if (implementations.contains(pathClass)) { + contextTypes.add(pathClass); + } + } + } + if (!contextTypes.isEmpty()) { + return contextTypes; + } + + // 2. Fallback: Inspect fields and local variables/parameters of the classes in the path + for (String methodFqn : currentPath) { + int lastDot = methodFqn.lastIndexOf('.'); + if (lastDot > 0) { + String pathClass = methodFqn.substring(0, lastDot); + if (pathClass.equals(declaringClassFqn) || implementations.contains(pathClass)) { + contextTypes.add(pathClass); + } + + TypeDeclaration td = context.getTypeDeclaration(pathClass); + if (td != null) { + // Check fields + for (FieldDeclaration fd : td.getFields()) { + String fieldType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(fd.getType()); + TypeDeclaration fieldTd = context.getTypeDeclaration(fieldType); + if (fieldTd == null) { + CompilationUnit cu = (CompilationUnit) td.getRoot(); + fieldTd = context.getTypeDeclaration(fieldType, cu); + } + if (fieldTd != null) { + String fieldFqn = context.getFqn(fieldTd); + if (fieldFqn.equals(declaringClassFqn) || implementations.contains(fieldFqn)) { + contextTypes.add(fieldFqn); + } + } + } + + // Check method parameters + for (MethodDeclaration md : td.getMethods()) { + for (Object paramObj : md.parameters()) { + if (paramObj instanceof SingleVariableDeclaration svd) { + String paramType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(svd.getType()); + TypeDeclaration paramTd = context.getTypeDeclaration(paramType); + if (paramTd == null) { + CompilationUnit cu = (CompilationUnit) td.getRoot(); + paramTd = context.getTypeDeclaration(paramType, cu); + } + if (paramTd != null) { + String paramFqn = context.getFqn(paramTd); + if (paramFqn.equals(declaringClassFqn) || implementations.contains(paramFqn)) { + contextTypes.add(paramFqn); + } + } + } + } + } + } + } + } + return contextTypes; + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolverTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolverTest.java index fc5938e..cab22b1 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolverTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolverTest.java @@ -482,4 +482,46 @@ public class ConstantResolverTest { assertThat(result).contains("OrderEvents.B2"); assertThat(result).contains("OrderEvents.DEF"); } + + @Test + void shouldResolveEnumValueOfWithConstantString(@TempDir Path tempDir) throws IOException { + Path dir = tempDir.resolve("com/example"); + Files.createDirectories(dir); + Files.writeString(dir.resolve("OrderEvent.java"), + "package com.example;\n" + + "public enum OrderEvent { RECEIVED, SHIPPED }\n"); + Files.writeString(dir.resolve("Caller.java"), + "package com.example;\n" + + "public class Caller {\n" + + " public void call() {\n" + + " OrderEvent e1 = OrderEvent.valueOf(\"RECEIVED\");\n" + + " OrderEvent e2 = OrderEvent.valueOf(getEventString());\n" + + " }\n" + + " private String getEventString() {\n" + + " return \"SHIPPED\";\n" + + " }\n" + + "}"); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller"); + MethodDeclaration callMethod = callerTd.getMethods()[0]; + + VariableDeclarationStatement vds1 = (VariableDeclarationStatement) callMethod.getBody().statements().get(0); + VariableDeclarationFragment fragment1 = (VariableDeclarationFragment) vds1.fragments().get(0); + MethodInvocation mi1 = (MethodInvocation) fragment1.getInitializer(); + + VariableDeclarationStatement vds2 = (VariableDeclarationStatement) callMethod.getBody().statements().get(1); + VariableDeclarationFragment fragment2 = (VariableDeclarationFragment) vds2.fragments().get(0); + MethodInvocation mi2 = (MethodInvocation) fragment2.getInitializer(); + + ConstantResolver resolver = new ConstantResolver(); + + String result1 = resolver.resolve(mi1, context); + assertThat(result1).isEqualTo("com.example.OrderEvent.RECEIVED"); + + String result2 = resolver.resolve(mi2, context); + assertThat(result2).isEqualTo("com.example.OrderEvent.SHIPPED"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineExtendedTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineExtendedTest.java index e8df2e6..59d1c57 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineExtendedTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineExtendedTest.java @@ -102,6 +102,7 @@ class HeuristicCallGraphEngineExtendedTest { java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source); CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); context.scan(tempDir); HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineGetterTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineGetterTest.java index 82f5735..da6e2e0 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineGetterTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineGetterTest.java @@ -954,4 +954,42 @@ class HeuristicCallGraphEngineGetterTest { assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) .containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED"); } + + @Test + void shouldResolveGetterOnInterProceduralClassInstanceCreation() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handle() { + service.process(new DomainEvent("PAYLOAD_ID")); + } + } + + class DomainEvent { + private String id; + public DomainEvent(String id) { this.id = id; } + public String getEvent() { return "INLINE_EVENT"; } + } + + class OrderService { + public void process(DomainEvent event) { + sendEvent(event.getEvent()); + } + public void sendEvent(String ev) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_interproc_cic"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handle").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("sendEvent").event("ev").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTypeTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTypeTest.java index e87978b..d9228ab 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTypeTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTypeTest.java @@ -468,7 +468,7 @@ class HeuristicCallGraphEngineTypeTest { CallChain chain = chains.get(0); assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("mapToEnum(str)"); assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .containsExactly(""); + .containsExactly(""); } @Test diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/PolymorphicDispatchCallGraphTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/PolymorphicDispatchCallGraphTest.java index 695cbe5..20ae4a5 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/PolymorphicDispatchCallGraphTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/PolymorphicDispatchCallGraphTest.java @@ -92,4 +92,153 @@ class PolymorphicDispatchCallGraphTest { "com.example.TransportOrderService.processOrderEvent" ); } + + @Test + void shouldFilterPolymorphicDispatchContextually(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + + public class LogisticsController { + private LogisticsService service; + + public void handle(String event) { + service.process(event); + } + } + + abstract class AbstractOrderService { + public void process(String event) { + String resolved = assertSupportedOrderEvent(event); + sendEvent(resolved); + } + + protected abstract String assertSupportedOrderEvent(String event); + + public void sendEvent(String ev) {} + } + + class LogisticsService extends AbstractOrderService { + @Override + protected String assertSupportedOrderEvent(String event) { + return LogisticsEvent.valueOf(event).name(); + } + } + + class ComputerStoreService extends AbstractOrderService { + @Override + protected String assertSupportedOrderEvent(String event) { + return ComputerEvent.valueOf(event).name(); + } + } + + enum LogisticsEvent { DELIVERED, RETURNED } + enum ComputerEvent { SHIPPED, REFUNDED } + """; + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.LogisticsController") + .methodName("handle") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.AbstractOrderService") + .methodName("sendEvent") + .event("ev") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("LogisticsEvent.DELIVERED", "LogisticsEvent.RETURNED"); + } + + @Test + void shouldResolveMapBasedDynamicDispatch(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + import java.util.Map; + import java.util.HashMap; + + public class DispatcherController { + private Map serviceMap = new HashMap<>(); + + public DispatcherController(LogisticsService logistics, ComputerStoreService computerStore) { + serviceMap.put("LOGISTICS", logistics); + serviceMap.put("COMPUTER", computerStore); + } + + public void handle(String machineType, String event) { + serviceMap.get(machineType).process(event); + } + } + + abstract class AbstractService { + public void process(String event) { + String resolved = assertSupportedEvent(event); + sendEvent(resolved); + } + protected abstract String assertSupportedEvent(String event); + public void sendEvent(String ev) {} + } + + class LogisticsService extends AbstractService { + @Override + protected String assertSupportedEvent(String event) { + return LogisticsEvent.valueOf(event).name(); + } + } + + class ComputerStoreService extends AbstractService { + @Override + protected String assertSupportedEvent(String event) { + return ComputerEvent.valueOf(event).name(); + } + } + + enum LogisticsEvent { DELIVERED, RETURNED } + enum ComputerEvent { SHIPPED, REFUNDED } + """; + Files.writeString(tempDir.resolve("DispatchConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.DispatcherController") + .methodName("handle") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.AbstractService") + .methodName("sendEvent") + .event("ev") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(2); + + CallChain chainLogistics = chains.stream() + .filter(c -> "machineType == \"LOGISTICS\"".equals(c.getTriggerPoint().getConstraint())) + .findFirst().orElseThrow(); + assertThat(chainLogistics.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("LogisticsEvent.DELIVERED", "LogisticsEvent.RETURNED"); + + CallChain chainComputer = chains.stream() + .filter(c -> "machineType == \"COMPUTER\"".equals(c.getTriggerPoint().getConstraint())) + .findFirst().orElseThrow(); + assertThat(chainComputer.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("ComputerEvent.SHIPPED", "ComputerEvent.REFUNDED"); + } } diff --git a/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json index c97874b..dfb404b 100644 --- a/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json @@ -282,7 +282,7 @@ }, "methodChain" : [ "click.kamil.web.RabbitOrderListener.handleCustomTransition", "click.kamil.service.StateMachineServiceImpl.sendCustomMessage" ], "triggerPoint" : { - "event" : "OrderEvent.PROCESS", + "event" : "new CustomStateMachineMessage<>(OrderEvent.PROCESS,\"My Rabbit Custom Payload: \" + payload)", "className" : "click.kamil.service.StateMachineServiceImpl", "methodName" : "sendCustomMessage", "sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java", @@ -290,16 +290,12 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 78, - "polymorphicEvents" : [ "OrderEvent.PROCESS" ], + "polymorphicEvents" : [ ], "external" : false, "constraint" : null }, "contextMachineId" : null, - "matchedTransitions" : [ { - "sourceState" : "OrderState.NEW", - "targetState" : "OrderState.PROCESSING", - "event" : "OrderEvent.PROCESS" - } ] + "matchedTransitions" : null }, { "entryPoint" : { "type" : "JMS",