58 Commits

Author SHA1 Message Date
901b43195a update 2026-06-27 20:25:29 +02:00
ed411f22d8 update 2026-06-27 20:20:49 +02:00
c9d41de13f update 2026-06-27 20:19:48 +02:00
df2a596e26 update 2026-06-27 20:16:20 +02:00
a75b7acfe4 update 2026-06-27 20:12:31 +02:00
74a4e66a33 update 2026-06-27 20:09:14 +02:00
4511e0e02f stage next 2026-06-27 20:07:24 +02:00
a7c3b08164 stage 3 2026-06-27 19:51:26 +02:00
8d66af6d24 stage 2 update loop 2026-06-27 19:33:44 +02:00
642f6f8926 stage 1 update loop 2026-06-27 19:19:59 +02:00
76bb88e065 stage 1 2026-06-27 19:18:32 +02:00
5c7cf3639b more tests fixes 2026-06-27 18:57:30 +02:00
016f0e9b21 more tests 2026-06-27 15:40:22 +02:00
22a0d3f5aa more bugs 2 2026-06-27 15:35:20 +02:00
7008f162b5 more bugs 2026-06-27 15:24:07 +02:00
60c666f7a1 better tests 2026-06-27 15:10:36 +02:00
8a56e92a65 more reorg 2026-06-27 14:50:06 +02:00
e8543ace38 tests reorg 2026-06-27 14:42:33 +02:00
101f2b5dc9 refactor 2026-06-27 14:32:10 +02:00
f525f3d3ce more fixes 2026-06-27 14:17:54 +02:00
5ae233eb75 claude bugs (question mark) 2026-06-27 07:19:18 +02:00
0bda5a0aeb gemini back 4 2026-06-27 05:56:23 +02:00
c35c75efa3 gemini back 3 2026-06-27 05:42:15 +02:00
bd55a66985 gemini back 2 2026-06-27 05:37:10 +02:00
0ac70bd3c6 gemini back 2026-06-27 05:29:12 +02:00
b45513c5fb heuristics updated enum collision 2026-06-25 06:39:31 +02:00
75fd45d983 heuristics updated 2026-06-25 05:45:49 +02:00
b35effd05c heuristics update 2026-06-25 05:45:49 +02:00
f30538e8c9 forward analysis nemotron3 ultra 2026-06-23 22:33:34 +02:00
131ccbeda0 forward analysis 2 2026-06-22 18:51:20 +02:00
14b311be2b forward analysis 2026-06-22 18:38:19 +02:00
1b690582d6 fixes 2 2026-06-22 14:11:35 +02:00
3ca834fa71 fixes 2026-06-22 13:53:27 +02:00
c6b4a4be1e better things extraction of constants 2 2026-06-22 13:12:48 +02:00
24a9be3a4e better things extraction of constants 2026-06-22 07:30:05 +02:00
07d241a060 better things 2026-06-21 22:55:39 +02:00
32de0a51c6 edge case fixes 2026-06-21 18:45:24 +02:00
cbb00e8a0f Fix AST constructor tracking gap for variables and fields 2026-06-21 18:36:37 +02:00
6596303b7d Replace fallback string scraper with precise AST traversal 2026-06-21 18:20:30 +02:00
c903b079d3 refactor 2026-06-21 17:53:26 +02:00
23d79d1930 event maching heuristic update 4 2026-06-21 15:54:36 +02:00
2720296d14 event maching heuristic update 3 2026-06-21 15:24:36 +02:00
b8b180ab3d event maching heuristic update 3 2026-06-21 14:58:28 +02:00
fc267c43c6 event maching heuristic update 2 2026-06-21 14:06:08 +02:00
968601eefc event maching heuristic update 2026-06-21 12:44:00 +02:00
e00f4dca81 beans fix more 2026-06-21 11:36:32 +02:00
bf82cc3562 beans fix 2026-06-21 09:00:36 +02:00
24b67be64b beans 2026-06-21 08:51:42 +02:00
9f651e7dc9 heuristics classpath 2026-06-21 07:51:25 +02:00
e06429cb02 heuristic call graph 2026-06-21 07:33:01 +02:00
9cf2d063b3 matching egnine extraction 2026-06-21 07:20:38 +02:00
073df21395 heauristic bean resolution 2026-06-21 06:55:55 +02:00
f9e6247eb3 fix 6 2026-06-20 13:20:05 +02:00
76ac143370 more fixes 5 2026-06-20 13:11:54 +02:00
9ca8b2fe37 more fixes 4 2026-06-20 12:31:11 +02:00
56cdf96f2d more fixes 3 2026-06-20 12:21:11 +02:00
c37aa92c78 more fixes 2 2026-06-20 07:57:03 +02:00
3ac057618f more fixes 2026-06-20 07:21:19 +02:00
145 changed files with 17734 additions and 2166 deletions

View File

@@ -1,71 +1,26 @@
# Execution Plan for Extended Analysis (Revised for Robustness)
## Phase 0: Surgical Refactoring (The Enrichment Hook)
- [x] Create `AnalysisResult` and `CodebaseMetadata` models.
- [x] Update `ExportService.generateOutputs` to accept `AnalysisResult`.
- [x] Implement `EnrichmentService` and `AnalysisEnricher` interface.
- [x] Wrap current parser outputs in `AnalysisResult` within `ExportService`.
## Phase 11: Formal Compiler & Static Analysis Alignment (Limitations & Missing Features)
While the custom AST-based `JdtDataFlowModel` supports advanced OOP data flow (like flow-sensitive local mutations, fluent builders, and polymorphic union fallbacks), the following compiler/static analysis behaviors are missing or simplified:
## Phase 1: Foundation & Semantic Core
- [ ] **JDT Binding Resolution**: Configure `ASTParser` with project classpath and `setResolveBindings(true)`.
- [ ] **Workspace Scoping**: Implement scanner with configurable exclude patterns (`test/`, `build/`, `node_modules/`).
- [ ] **ConstantResolver**: Implement lookup for `public static final` constants across class boundaries using JDT bindings.
- [ ] **Basic PropertyResolver**: Scan `application.properties/yml` for simple key-value pairs.
- [ ] Implement `JdtIntelligenceProvider` as the primary semantic analyzer.
1. **Full Pointer & Alias Analysis**:
- If two variables alias the same object (e.g., `Command cmd1 = cmd2;`), a mutation on `cmd1` (e.g., `cmd1.setEvent("A")`) is not reflected when reading the fields of `cmd2` unless they resolve to the exact same AST node reference. A formal pointer/alias analysis (like Anderson's or Steensgaard's algorithm) would map abstract heap locations rather than AST nodes.
## Phase 2: Trigger Discovery & Instance Identity
- [ ] **GenericEventDetector**: Find `sendEvent` calls using `ASTVisitor`.
- [ ] **InstanceIdentifier**: Detect which State Machine is being targeted (via `@Qualifier`, bean names, or generic types `StateMachine<S, E>`).
- [ ] **Trigger Filter**: Link `sendEvent` calls to specific SM instances to avoid noise in multi-SM projects.
- [ ] Enhance `CodebaseContext` to provide mapping from `MethodDeclaration` to its callers using bindings.
2. **Advanced Loop Fixpoints for Fields**:
- If a loop mutates a field based on its previous values (e.g., `while(cond) { builder.count(builder.getCount() + 1); }`), a simple sequential pass will miss the cyclic dependency. A formal data flow framework uses a mathematical lattice and runs a fixpoint iteration algorithm (using meet/join operators) to compute the stable approximation of values.
## Phase 3: Entry Point Mapping
- [ ] **SpringMvcDetector**: Detect REST endpoints (using `ConstantResolver` for paths).
- [ ] **MessageListenerDetector**: Detect Kafka/JMS listeners (using `ConstantResolver` for topics/queues).
- [ ] **ConfigurableTriggerDetector**: Support user-defined patterns for custom frameworks.
3. **Exception Path Analysis (Exception CFG)**:
- Edges for exception flows (`throw` statements, `try-catch-finally` blocks, and implicit unchecked runtime exceptions like `NullPointerException`) are not modeled. Consequently, mutations or return paths within catch/finally blocks may be missed or evaluated out of order.
## Phase 4: Call Graph Integration
- [x] Develop a call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls.
- [ ] Support inheritance in call graph resolution (using JDT bindings to resolve method overrides).
4. **Collection & Array Tracking**:
- Arrays and Collections (such as `List`, `Map`, `Set`) are treated as opaque objects, and their operations (like `list.add(val)` or `map.put(key, val)`) are evaluated as general method calls. Modifying a collection does not propagate the values to its getter calls (e.g. `list.get(0)`).
## Phase 4.5: Hierarchical Robustness (Inheritance Support)
- [ ] **Hierarchical Annotation Lookup**: Update `SpringMvcDetector` to scan interfaces and superclasses for inherited mapping annotations.
- [ ] **Interface Implementation Mapping**: Enhance `CodebaseContext` to index "Class -> Interfaces" and "Interface -> Classes" relationships.
- [ ] **Polymorphic Call Trace**: Update `CallGraphBuilder` to follow calls from an interface method to all known implementations in the project.
- [ ] **Base Class Trigger Discovery**: Ensure triggers in abstract base classes are correctly attributed to their concrete subclasses.
5. **Field Shadowing and Inheritance Semantics**:
- If a subclass shadows a superclass field (declaring a field with the same name), our resolver might resolve the wrong field binding. Also, implicit superclass constructors and field initializers from superclass hierarchies are not fully simulated.
6. **Static Initialization Blocks and Static Mutable State**:
- Class loading execution order (i.e. static block initializations `static { ... }`) is not modeled. A mutation on a static field in one method is not propagated to reads in another method.
7. **Type Erasure & Generics Precision**:
- Generics type bounds (e.g., wildcards `? extends T`, type parameter replacements) are not fully tracked, which can result in incorrect polymorphic resolution when method signatures differ slightly from erased bindings.
## Phase 5: Visualization & Diagnostics
- [ ] Update `ExportService` to include trigger information.
- [ ] Update DOT/SCXML exporters to show entry points.
- [ ] **Diagnostic Nodes**: Show "Potential/Unresolved Trigger" nodes when a `sendEvent` is found but the event name or SM instance cannot be resolved.
## Phase 6: Validation (Milestone 1)
- [ ] Create a "Complex Sample Project" with REST controllers, services, and multiple state machines.
- [ ] Add unit tests for each detector and the aggregator.
- [ ] Verify that inheritance in both state machine config and controllers is correctly handled.
## Phase 7: Advanced Resolution & Data Flow
- [ ] **Variable Propagation**: Track values from fields/constructors to their usage in the State Machine configuration (e.g., resolving `initial(initialState)` where `initialState` is a field).
- [ ] **@Value Resolution**: Link `@Value` annotations to the `PropertyResolver` to substitute placeholders with literal values in the diagram.
- [ ] **Spring Profile Support**: Update `PropertyResolver` to handle `application-{profile}.properties` and prioritize them based on an "active profiles" setting.
- [ ] **Complex ValueResolver**: Handle string concatenations (e.g., `"PREFIX_" + MyConstants.SUFFIX`) and variable references.
- [ ] **Payload Analysis**: Extract payload types from entry points and track field usage in `sendEvent` calls.
## Phase 8: Persistence & Lifecycle Mapping
- [ ] Detect persistence restoration logic (`persister.restore`).
- [ ] Map "Resume" points where the machine state is loaded from a DB based on external identifiers.
- [ ] Update metadata to distinguish between "New Instance" and "Restored Instance" triggers.
## Phase 9: Advanced Ecosystem Support
- [ ] **Reactive Chains**: Analyze WebFlux/Project Reactor lambdas (`doOnNext`, `flatMap`) for hidden triggers.
- [ ] **Interceptors & Filters**: Detect `HandlerInterceptor` and `Filter` implementations and map them to endpoints.
## Phase 10: External Dependency Interop
- [x] **Library Hints**: Implementation complete. Supports `hints.json` in project root or `src/main/resources/`.
- **Format**: `[{"methodFqn": "class.method", "event": "EVENT"}]`
- **Purpose**: Maps external/compiled library calls to State Machine events when source code is unavailable.
- [ ] **External JAR Analysis**: (Optional) Configure JDT to scan source-attachments of dependencies if available.
**Note on Library Hints**: This feature is an optional "Power User" escape hatch. The analyzer works automatically for all local source code; `hints.json` is only required to bridge the gap for third-party libraries.

View File

@@ -14,8 +14,16 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.matching.HeuristicEventMatchingEngine;
public class TransitionLinkerEnricher implements AnalysisEnricher {
private final BeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();
private final EventMatchingEngine matchingEngine = new HeuristicEventMatchingEngine();
@Override
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
if (result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
@@ -38,41 +46,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
List<MatchedTransition> matched = new ArrayList<>();
for (Transition t : stateMachineTransitions) {
if (t.getEvent() != null) {
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)) {
String smEventRaw = t.getEvent().fullIdentifier() != null ? t.getEvent().fullIdentifier() : t.getEvent().rawName();
String smEvent = simplify(smEventRaw);
boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") ||
triggerEvent.equals("msg") || triggerEvent.equals("message") ||
triggerEvent.equals("payload");
if (isWildcard) {
String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId();
}
// We no longer hard-block wildcards without a specific routing context.
// If a project doesn't use standard SM persisters (e.g. restores state manually),
// contextMachineId will be null. We should still link the wildcard to provide SOME visibility,
// rather than completely hiding the endpoint.
}
List<String> polyEvents = tp.getPolymorphicEvents() != null ? tp.getPolymorphicEvents() : java.util.Collections.emptyList();
boolean hasPolyMatch = false;
for (String pe : polyEvents) {
String simplePe = pe;
if (pe.contains(".")) {
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
}
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
hasPolyMatch = true;
break;
}
}
if (hasPolyMatch || (polyEvents.isEmpty() && (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())))) {
// Event matches or is a wildcard
for (State smSourceState : t.getSourceStates()) {
// Event matches
for (State smSourceState : t.getSourceStates()) {
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
String smSource = simplify(smSourceRaw);
if (triggerSource == null || triggerSource.equals(smSource)) {
@@ -102,7 +79,6 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
}
}
}
}
if (!matched.isEmpty()) {
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
@@ -123,29 +99,12 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
result.setMetadata(updatedMetadata);
}
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
// If the chain's target expression or qualifier indicates a specific machine name, verify it
String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId();
}
if (targetVar != null && !targetVar.isEmpty()) {
targetVar = targetVar.toLowerCase();
// E.g., if target is "myStateMachine", ensure current machine name contains "my"
String simplifiedMachineName = currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase();
// If the variable name ends with StateMachine, we extract the prefix
if (targetVar.endsWith("statemachine")) {
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
if (!prefix.isEmpty() && !simplifiedMachineName.contains(prefix)) {
return false;
}
}
}
return true;
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName);
}
private String simplify(String name) {
private String simplify(String name) {
if (name == null) return null;
// Strip common suffixes
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
@@ -157,4 +116,31 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
return simplified;
}
}
private boolean isGuardedContains(String smEvent, String triggerEvent) {
// Only fire if it looks like a simple identifier (no spaces, no parens)
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
return false;
}
String shorter = smEvent.length() < triggerEvent.length() ? smEvent : triggerEvent;
String longer = smEvent.length() >= triggerEvent.length() ? smEvent : triggerEvent;
shorter = shorter.toLowerCase();
longer = longer.toLowerCase();
if (longer.contains(shorter)) {
// Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators
// to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE")
if (shorter.length() < 4) {
return longer.matches(".*(^|_|-)" + shorter + "(_|-|$).*");
}
// For longer strings, a straight contains is usually safe enough
return true;
}
return false;
}
}

View File

@@ -0,0 +1,11 @@
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.model.Event;
public interface EventMatchingEngine {
/**
* Determines whether the given trigger point is a match for the state machine event.
*/
boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint);
}

View File

@@ -0,0 +1,75 @@
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.model.Event;
import java.util.List;
public class HeuristicEventMatchingEngine implements EventMatchingEngine {
@Override
public boolean matches(Event stateMachineEvent, TriggerPoint triggerPoint) {
if (stateMachineEvent == null || triggerPoint == null || triggerPoint.getEvent() == null) {
return false;
}
String rawTriggerEvent = triggerPoint.getEvent();
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
String smEvent = simplify(smEventRaw);
boolean isWildcard = isWildcardVariable(rawTriggerEvent);
List<String> polyEvents = triggerPoint.getPolymorphicEvents() != null ? triggerPoint.getPolymorphicEvents() : java.util.Collections.emptyList();
boolean hasPolyMatch = false;
for (String pe : polyEvents) {
if (pe.contains(".") && smEventRaw.contains(".")) {
if (smEventRaw.equals(pe) || smEventRaw.endsWith("." + pe) || pe.endsWith("." + smEventRaw)) {
hasPolyMatch = true;
break;
}
continue;
}
String simplePe = pe;
if (pe.contains(".")) {
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
}
String simplifiedPe = simplify(simplePe);
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) {
hasPolyMatch = true;
break;
}
}
if (hasPolyMatch) return true;
if (polyEvents.isEmpty() && isWildcard) return true;
if (rawTriggerEvent.contains(".") && smEventRaw.contains(".")) {
if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent) || rawTriggerEvent.endsWith("." + smEventRaw)) {
return true;
}
return false;
}
String triggerEvent = simplify(rawTriggerEvent);
return smEvent.equals(triggerEvent);
}
private boolean isWildcardVariable(String eventStr) {
return eventStr.equals("event") || eventStr.equals("e") ||
eventStr.equals("msg") || eventStr.equals("message") ||
eventStr.equals("payload");
}
private String simplify(String name) {
if (name == null) return null;
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
if (simplified.isEmpty()) {
simplified = name;
}
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
return simplified;
}
}

View File

@@ -0,0 +1,69 @@
package click.kamil.springstatemachineexporter.analysis.enricher.path;
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class ForwardPathEstimationEnricher implements AnalysisEnricher {
private final boolean isEnabled;
private final PathValueEstimator estimator;
public ForwardPathEstimationEnricher() {
this.isEnabled = Boolean.parseBoolean(System.getProperty("analyzer.forward-path-estimation.enabled", "false"));
this.estimator = new SymbolicPathValueEstimator();
}
@Override
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
if (!isEnabled || result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
return;
}
log.info("Running Symbolic Forward Path Analysis for {}", result.getName());
List<CallChain> chains = result.getMetadata().getCallChains();
List<CallChain> updatedChains = chains.stream().map(chain -> {
if (chain.getTriggerPoint() == null || chain.getTriggerPoint().getEvent() == null) return chain;
// If the event looks like a dynamic variable (no dots, not an enum format)
if (!chain.getTriggerPoint().getEvent().contains(".")) {
List<String> estimatedValues = estimator.estimatePossibleValues(chain, context);
if (estimatedValues != null && !estimatedValues.isEmpty()) {
log.info("Symbolic Path Analysis estimated values for {} -> {}", chain.getTriggerPoint().getEvent(), estimatedValues);
List<String> polyEvents = new ArrayList<>();
if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
polyEvents.addAll(chain.getTriggerPoint().getPolymorphicEvents());
}
polyEvents.addAll(estimatedValues);
TriggerPoint newTp = chain.getTriggerPoint().toBuilder()
.polymorphicEvents(polyEvents)
.build();
return chain.toBuilder().triggerPoint(newTp).build();
}
}
return chain;
}).collect(Collectors.toList());
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata updatedMetadata =
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
.triggers(result.getMetadata().getTriggers())
.entryPoints(result.getMetadata().getEntryPoints())
.callChains(updatedChains)
.properties(result.getMetadata().getProperties())
.build();
result.setMetadata(updatedMetadata);
}
}

View File

@@ -0,0 +1,10 @@
package click.kamil.springstatemachineexporter.analysis.enricher.path;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.List;
public interface PathValueEstimator {
List<String> estimatePossibleValues(CallChain chain, CodebaseContext context);
}

View File

@@ -0,0 +1,130 @@
package click.kamil.springstatemachineexporter.analysis.enricher.path;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SymbolicPathValueEstimator implements PathValueEstimator {
@Override
public List<String> estimatePossibleValues(CallChain chain, CodebaseContext context) {
if (chain.getMethodChain() == null || chain.getMethodChain().isEmpty()) {
return null;
}
Set<String> collectedConstants = new HashSet<>();
for (String methodFqn : chain.getMethodChain()) {
int lastDot = methodFqn.lastIndexOf('.');
if (lastDot == -1) continue;
String className = methodFqn.substring(0, lastDot);
String methodName = methodFqn.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) continue;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null || md.getBody() == null) continue;
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if ((node.getName().getIdentifier().equals("equals") || node.getName().getIdentifier().equals("equalsIgnoreCase"))
&& node.arguments().size() == 1) {
Expression caller = node.getExpression();
Expression arg = (Expression) node.arguments().get(0);
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
String callerVal = resolver.resolve(caller, context);
if (callerVal != null && !callerVal.isEmpty()) {
addValue(collectedConstants, callerVal);
}
String argVal = resolver.resolve(arg, context);
if (argVal != null && !argVal.isEmpty()) {
addValue(collectedConstants, argVal);
}
}
return super.visit(node);
}
@Override
public boolean visit(InfixExpression node) {
if (node.getOperator() == InfixExpression.Operator.EQUALS) {
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
String left = resolver.resolve(node.getLeftOperand(), context);
if (left != null && !left.isEmpty()) {
addValue(collectedConstants, left);
}
String right = resolver.resolve(node.getRightOperand(), context);
if (right != null && !right.isEmpty()) {
addValue(collectedConstants, right);
}
}
return super.visit(node);
}
@Override
public boolean visit(SwitchStatement node) {
for (Object stmt : node.statements()) {
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
for (Object expr : sc.expressions()) {
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
String val = resolver.resolve((Expression) expr, context);
if (val != null && !val.isEmpty()) {
addValue(collectedConstants, val);
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SwitchExpression node) {
for (Object stmt : node.statements()) {
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
for (Object expr : sc.expressions()) {
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
String val = resolver.resolve((Expression) expr, context);
if (val != null && !val.isEmpty()) {
addValue(collectedConstants, val);
}
}
}
}
return super.visit(node);
}
});
}
if (collectedConstants.isEmpty()) return null;
return new ArrayList<>(collectedConstants);
}
private void addValue(Set<String> collectedConstants, String val) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
String parsed = parseEnumSetElement(eVal);
if (!collectedConstants.contains(parsed)) collectedConstants.add(parsed);
}
} else {
collectedConstants.add(val);
}
}
private String parseEnumSetElement(String element) {
if (element.contains(".")) {
return element.substring(element.lastIndexOf('.') + 1);
}
return element;
}
}

View File

@@ -0,0 +1,7 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
public interface BeanResolutionEngine {
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName);
}

View File

@@ -0,0 +1,203 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import java.util.HashSet;
import java.util.Set;
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
@Override
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName) {
String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId();
}
String simplifiedMachineName = currentMachineName != null ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() : "";
if (targetVar != null && !targetVar.isEmpty()) {
targetVar = targetVar.toLowerCase();
if (targetVar.endsWith("statemachine")) {
String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length());
if (!prefix.isEmpty()) {
if (simplifiedMachineName.contains(prefix)) {
return true; // Explicit positive match
} else {
return false; // Explicit negative match
}
}
} else if (simplifiedMachineName.contains(targetVar)) {
return true;
}
}
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) {
String smPackage = getPackageName(currentMachineName);
String smSimple = getSimpleClassName(currentMachineName);
String smPrefix = getFirstCamelCaseWord(smSimple);
boolean hasPositiveMatch = false;
boolean hasStrongMismatch = false;
if (chain.getTriggerPoint() != null && chain.getTriggerPoint().getClassName() != null) {
String chainClass = chain.getTriggerPoint().getClassName();
String chainPackage = getPackageName(chainClass);
String chainSimple = getSimpleClassName(chainClass);
String chainPrefix = getFirstCamelCaseWord(chainSimple);
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
hasPositiveMatch = true;
}
if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
hasPositiveMatch = true;
}
}
for (String method : chain.getMethodChain()) {
String chainClass = getClassNameOnly(method);
String chainPackage = getPackageName(chainClass);
String chainSimple = getSimpleClassName(chainClass);
String chainPrefix = getFirstCamelCaseWord(chainSimple);
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
hasPositiveMatch = true;
}
if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
hasPositiveMatch = true;
}
if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix)) {
hasStrongMismatch = true;
}
}
if (hasPositiveMatch) {
return true;
}
if (hasStrongMismatch) {
return false;
}
}
return true;
}
private boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
if (smPackage.startsWith(chainPackage) || chainPackage.startsWith(smPackage)) {
return true;
}
// Find deepest common package root
String[] p1 = smPackage.split("\\.");
String[] p2 = chainPackage.split("\\.");
int matchIdx = -1;
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
if (p1[i].equals(p2[i])) {
matchIdx = i;
} else {
break;
}
}
// If the state machine's prefix is part of the shared common package,
// then the state machine represents the parent domain of the caller.
if (smPrefix != null && matchIdx >= 0) {
String lowerPrefix = smPrefix.toLowerCase();
for (int i = 0; i <= matchIdx; i++) {
if (p1[i].toLowerCase().contains(lowerPrefix) || lowerPrefix.contains(p1[i].toLowerCase())) {
return true;
}
}
}
return false;
}
private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) {
if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false;
String[] p1 = smPackage.split("\\.");
String[] p2 = chainPackage.split("\\.");
int matchIdx = -1;
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
if (p1[i].equals(p2[i])) {
matchIdx = i;
} else {
break;
}
}
Set<String> smDivergentTerms = new HashSet<>();
if (smPrefix != null) smDivergentTerms.add(smPrefix.toLowerCase());
for (int i = matchIdx + 1; i < p1.length; i++) {
smDivergentTerms.add(p1[i].toLowerCase());
}
Set<String> chainDivergentTerms = new HashSet<>();
if (chainPrefix != null) chainDivergentTerms.add(chainPrefix.toLowerCase());
for (int i = matchIdx + 1; i < p2.length; i++) {
chainDivergentTerms.add(p2[i].toLowerCase());
}
if (!smDivergentTerms.isEmpty() && !chainDivergentTerms.isEmpty()) {
Set<String> intersection = new HashSet<>(smDivergentTerms);
intersection.retainAll(chainDivergentTerms);
return intersection.isEmpty();
}
return false;
}
private Set<String> extractDomainTerms(String pkg, String prefix) {
Set<String> terms = new HashSet<>();
if (pkg != null && !pkg.isEmpty()) {
String[] segments = pkg.split("\\.");
// Take segments from index 2 onwards if length > 2
int start = segments.length > 2 ? 2 : 0;
for (int i = start; i < segments.length; i++) {
terms.add(segments[i].toLowerCase());
}
}
if (prefix != null && !prefix.isEmpty()) {
terms.add(prefix.toLowerCase());
}
return terms;
}
private String getPackageName(String fqn) {
if (fqn == null || !fqn.contains(".")) return "";
return fqn.substring(0, fqn.lastIndexOf('.'));
}
private String getSimpleClassName(String fqn) {
if (fqn == null) return "";
if (!fqn.contains(".")) return fqn;
return fqn.substring(fqn.lastIndexOf('.') + 1);
}
private String getClassNameOnly(String methodFqn) {
if (methodFqn == null) return "";
String clean = methodFqn;
if (clean.contains("(")) {
clean = clean.substring(0, clean.indexOf('('));
}
if (clean.contains(".")) {
clean = clean.substring(0, clean.lastIndexOf('.'));
}
return clean;
}
private String getFirstCamelCaseWord(String simpleName) {
if (simpleName == null || simpleName.isEmpty()) return null;
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
if (words.length > 0) {
return words[0];
}
return null;
}
}

View File

@@ -0,0 +1,20 @@
package click.kamil.springstatemachineexporter.analysis.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CallEdge {
private String targetMethod;
private List<String> arguments;
private String receiver;
public CallEdge(String targetMethod, List<String> arguments) {
this(targetMethod, arguments, null);
}
}

View File

@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
import java.util.Map;
@Data
@Builder
@Builder(toBuilder = true)
@Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true)
public class TriggerPoint {

View File

@@ -14,9 +14,30 @@ public class ConstantResolver {
return resolveInternal(expr, context, new HashSet<>());
}
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context) {
return evaluateSwitchExpression(se, paramValues, context, new HashSet<>());
}
public String evaluateSwitchWithParams(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context) {
return evaluateSwitchStatement(ss, paramValues, context, new HashSet<>());
}
private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) {
if (expr == null) return null;
// Unwrap common wrappers
if (expr instanceof CastExpression ce) {
return resolveInternal(ce.getExpression(), context, visited);
}
if (expr instanceof ParenthesizedExpression pe) {
return resolveInternal(pe.getExpression(), context, visited);
}
if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
if (!mi.arguments().isEmpty()) {
return resolveInternal((Expression) mi.arguments().get(0), context, visited);
}
}
// 1. Literal?
if (expr instanceof StringLiteral sl) {
return sl.getLiteralValue();
@@ -51,11 +72,40 @@ public class ConstantResolver {
String val = resolveManual(qn, context, visited);
return val != null ? val : qn.toString();
}
if (expr instanceof FieldAccess fa) {
return resolveManual(fa.getName(), context, visited);
}
if (expr instanceof SimpleName sn) {
return resolveManual(sn, context, visited);
}
if (expr instanceof MethodInvocation mi) {
if ((mi.getName().getIdentifier().equals("name") || mi.getName().getIdentifier().equals("toString")) && mi.arguments().isEmpty()) {
String val = resolveInternal(mi.getExpression(), context, visited);
if (val != null) {
// If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it
if (val.contains(".")) {
return val.substring(val.lastIndexOf('.') + 1);
}
return val;
}
// Fallback for unresolved AST nodes
if (mi.getExpression() instanceof QualifiedName qn) {
System.out.println("RETURNING qn name: " + qn.getName().getIdentifier()); return qn.getName().getIdentifier();
} else if (mi.getExpression() instanceof SimpleName sn) {
System.out.println("RETURNING sn name: " + sn.getIdentifier()); return sn.getIdentifier();
}
}
if ("valueOf".equals(mi.getName().getIdentifier()) && mi.getExpression() != null) {
java.util.List<String> enumValues = context.getEnumValues(mi.getExpression().toString());
if (enumValues != null && !enumValues.isEmpty()) {
return "ENUM_SET:" + String.join(",", enumValues);
}
}
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
ITypeBinding returnType = mb.getReturnType();
@@ -66,13 +116,43 @@ public class ConstantResolver {
}
}
} else {
TypeDeclaration td = findEnclosingType(mi);
TypeDeclaration td = null;
if (mi.getExpression() != null) {
ITypeBinding typeBinding = mi.getExpression().resolveTypeBinding();
if (typeBinding != null) {
String typeName = typeBinding.getQualifiedName();
if (typeName != null && !typeName.isEmpty()) {
td = context.getTypeDeclaration(typeName);
}
}
}
if (td == null && mi.getExpression() instanceof SimpleName sn) {
String typeName = resolveLocalType(sn);
if (typeName != null) {
td = context.getTypeDeclaration(typeName);
if (td == null) {
CompilationUnit cu = (CompilationUnit) mi.getRoot();
td = context.getTypeDeclaration(typeName, cu);
}
}
}
if (td == null) {
td = findEnclosingType(mi);
}
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
if (md != null && md.getReturnType2() != null) {
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
if (values != null && !values.isEmpty()) {
return "ENUM_SET:" + String.join(",", values);
if (md != null) {
String evaluated = evaluateMethodOutput(mi, md, context, visited);
if (evaluated != null) return evaluated;
if (md.getReturnType2() != null) {
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
if (values != null && !values.isEmpty()) {
return "ENUM_SET:" + String.join(",", values);
}
}
}
}
@@ -82,6 +162,269 @@ public class ConstantResolver {
return null;
}
private String evaluateMethodOutput(MethodInvocation mi, MethodDeclaration md, CodebaseContext context, Set<String> visited) {
if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null;
TypeDeclaration td = findEnclosingType(md);
if (td == null) return null;
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
if (!visited.add(methodFqn)) {
return null; // Cycle detected in method evaluation
}
try {
java.util.Map<String, String> localVars = new java.util.HashMap<>();
java.util.Set<String> declaredLocals = new java.util.HashSet<>();
for (int i = 0; i < mi.arguments().size(); i++) {
Expression arg = (Expression) mi.arguments().get(i);
String val = resolveInternal(arg, context, visited);
if (val != null) {
org.eclipse.jdt.core.dom.SingleVariableDeclaration param =
(org.eclipse.jdt.core.dom.SingleVariableDeclaration) md.parameters().get(i);
String paramName = param.getName().getIdentifier();
localVars.put(paramName, val);
declaredLocals.add(paramName);
}
}
return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited);
} finally {
visited.remove(methodFqn);
}
}
/**
* Evaluates a method body with pre-supplied local values (e.g. field values derived
* from the specific {@code ClassInstanceCreation} that created the receiver object).
* Used by {@link click.kamil.springstatemachineexporter.analysis.service.AbstractCallGraphEngine}
* to resolve transforming getters — methods that map a constructor-injected enum to a
* different state-machine enum via switch expressions.
*/
public String evaluateMethodBodyWithLocals(MethodDeclaration md,
java.util.Map<String, String> prebuiltLocals,
CodebaseContext context,
Set<String> visited) {
if (md.getBody() == null) return null;
TypeDeclaration td = findEnclosingType(md);
if (td == null) return null;
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
if (!visited.add(methodFqn)) {
return null; // Cycle detected in method evaluation
}
try {
java.util.Map<String, String> localVars = new java.util.HashMap<>(prebuiltLocals);
java.util.Set<String> declaredLocals = new java.util.HashSet<>(prebuiltLocals.keySet());
return evaluateBody(md.getBody(), localVars, declaredLocals, context, visited);
} finally {
visited.remove(methodFqn);
}
}
/**
* Core body-evaluation logic shared by {@link #evaluateMethodOutput} and
* {@link #evaluateMethodBodyWithLocals}. Visits the block, tracks assignments,
* evaluates switch statements/expressions, and captures the first resolved return value.
*/
private String evaluateBody(org.eclipse.jdt.core.dom.Block body,
java.util.Map<String, String> localVars,
java.util.Set<String> declaredLocals,
CodebaseContext context,
Set<String> visited) {
String[] finalResult = new String[1];
body.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
String varName = fragment.getName().getIdentifier();
declaredLocals.add(varName);
if (fragment.getInitializer() != null) {
String rhsVal = resolveExpressionWithParams(fragment.getInitializer(), localVars);
if (rhsVal == null) rhsVal = resolveInternal(fragment.getInitializer(), context, visited);
if (rhsVal != null) localVars.put(varName, rhsVal);
}
}
}
return super.visit(vds);
}
@Override
public boolean visit(Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
String varName = null;
if (lhs instanceof SimpleName sn) {
varName = sn.getIdentifier();
} else if (lhs instanceof FieldAccess fa) {
varName = "this." + fa.getName().getIdentifier();
}
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars);
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
if (varName != null && rhsVal != null) {
if (varName.startsWith("this.")) {
localVars.put(varName, rhsVal);
String bareName = varName.substring(5);
if (!declaredLocals.contains(bareName)) localVars.put(bareName, rhsVal);
} else {
localVars.put(varName, rhsVal);
if (!declaredLocals.contains(varName)) localVars.put("this." + varName, rhsVal);
}
}
return super.visit(assignment);
}
@Override
public boolean visit(org.eclipse.jdt.core.dom.SwitchStatement ss) {
if (finalResult[0] == null) {
String result = evaluateSwitchStatement(ss, localVars, context, visited);
if (result != null) finalResult[0] = result;
}
return false; // children handled inside evaluateSwitchStatement
}
@Override
public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) {
if (finalResult[0] == null) {
if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
String result = evaluateSwitchExpression(se, localVars, context, visited);
if (result != null) finalResult[0] = result;
} else if (rs.getExpression() != null) {
String result = resolveExpressionWithParams(rs.getExpression(), localVars);
if (result == null) result = resolveInternal(rs.getExpression(), context, visited);
if (result != null) finalResult[0] = result;
}
}
return super.visit(rs);
}
});
return finalResult[0];
}
private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues);
if (switchVar == null) return null;
String switchType = null;
if (switchVar.contains(".")) {
switchType = switchVar.substring(0, switchVar.lastIndexOf('.'));
}
boolean matchingCase = false;
for (Object stmtObj : ss.statements()) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
matchingCase = false;
if (sc.isDefault()) {
matchingCase = true;
} else {
for (Object exprObj : sc.expressions()) {
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
caseVal = caseSn.getIdentifier();
if (switchType != null) {
caseVal = switchType + "." + caseVal;
}
}
if (caseVal != null) {
if (switchVar.contains(".") && caseVal.contains(".")) {
if (switchVar.equals(caseVal)) {
matchingCase = true;
break;
}
} else if (switchVar.endsWith(caseVal)) {
matchingCase = true;
break;
}
}
}
}
} else if (matchingCase) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
return resolveInternal(rs.getExpression(), context, visited);
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) {
return resolveInternal(ys.getExpression(), context, visited);
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
return resolveInternal(es.getExpression(), context, visited);
}
}
}
return null;
}
private String evaluateSwitchExpression(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues);
if (switchVar == null) return null;
String switchType = null;
if (switchVar.contains(".")) {
switchType = switchVar.substring(0, switchVar.lastIndexOf('.'));
}
boolean matchingCase = false;
for (Object stmtObj : se.statements()) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
matchingCase = false;
if (sc.isDefault()) {
matchingCase = true;
} else {
for (Object exprObj : sc.expressions()) {
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues);
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
caseVal = caseSn.getIdentifier();
if (switchType != null) {
caseVal = switchType + "." + caseVal;
}
}
if (caseVal != null) {
if (switchVar.contains(".") && caseVal.contains(".")) {
if (switchVar.equals(caseVal)) {
matchingCase = true;
break;
}
} else if (switchVar.endsWith(caseVal)) {
matchingCase = true;
break;
}
}
}
}
} else if (matchingCase) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
return resolveInternal(es.getExpression(), context, visited);
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) {
return resolveInternal(ys.getExpression(), context, visited);
} else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
return resolveInternal(rs.getExpression(), context, visited);
}
}
}
return null;
}
private String resolveExpressionWithParams(Expression expr, java.util.Map<String, String> paramValues) {
if (expr instanceof SimpleName sn) {
String name = sn.getIdentifier();
if (paramValues.containsKey(name)) return paramValues.get(name);
return null;
} else if (expr instanceof FieldAccess fa) {
String name = "this." + fa.getName().getIdentifier();
if (paramValues.containsKey(name)) return paramValues.get(name);
return null;
} else if (expr instanceof QualifiedName qn) {
return null;
} else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) {
return sl.getLiteralValue();
}
return null;
}
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
@@ -102,6 +445,32 @@ public class ConstantResolver {
}
private String resolveManual(SimpleName sn, CodebaseContext context, Set<String> visited) {
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
if (enclosingMethod != null) {
for (Object paramObj : enclosingMethod.parameters()) {
if (paramObj instanceof SingleVariableDeclaration svd) {
if (svd.getName().getIdentifier().equals(sn.getIdentifier())) {
return null;
}
}
}
if (enclosingMethod.getBody() != null) {
boolean[] isLocal = {false};
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(sn.getIdentifier())) {
isLocal[0] = true;
}
return super.visit(node);
}
});
if (isLocal[0]) {
return null;
}
}
}
TypeDeclaration td = findEnclosingType(sn);
if (td != null) {
String fqn = context.getFqn(td);
@@ -161,6 +530,28 @@ public class ConstantResolver {
}
}
}
// Check constructors for assignments
for (MethodDeclaration md : td.getMethods()) {
if (md.isConstructor() && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ExpressionStatement es) {
if (es.getExpression() instanceof Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
boolean matches = false;
if (lhs instanceof SimpleName snLhs && snLhs.getIdentifier().equals(fieldName)) {
matches = true;
} else if (lhs instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName) && fa.getExpression() instanceof ThisExpression) {
matches = true;
}
if (matches) {
return resolveInternal(assignment.getRightHandSide(), context, visited);
}
}
}
}
}
}
} finally {
visited.remove(fieldId);
}
@@ -206,4 +597,49 @@ public class ConstantResolver {
}
return (TypeDeclaration) parent;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null) {
if (parent instanceof MethodDeclaration md) {
return md;
}
parent = parent.getParent();
}
return null;
}
private String resolveLocalType(SimpleName sn) {
String varName = sn.getIdentifier();
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
if (enclosingMethod != null && enclosingMethod.getBody() != null) {
String[] typeName = new String[1];
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
typeName[0] = node.getType().toString();
}
}
return super.visit(node);
}
});
if (typeName[0] != null) return typeName[0];
}
TypeDeclaration enclosingType = findEnclosingType(sn);
if (enclosingType != null) {
for (FieldDeclaration field : enclosingType.getFields()) {
for (Object fragObj : field.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
return field.getType().toString();
}
}
}
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import java.util.List;
public interface PolymorphicEventResolver {
/**
* Attempts to resolve a dynamically passed event variable into a set of
* concrete polymorphic event names.
*
* @param triggerPoint The original trigger point to resolve.
* @param resolvedValue The current String expression (e.g., "event.getType()" or "event")
* @param path The call path trace leading up to this variable.
* @param context The codebase context for deep type resolution.
* @return The updated TriggerPoint with polymorphic events set, or the original if unable to resolve.
*/
TriggerPoint resolvePolymorphicEvents(TriggerPoint triggerPoint, String resolvedValue, List<String> path, CodebaseContext context);
}

View File

@@ -0,0 +1,965 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
@Slf4j
public abstract class AbstractCallGraphEngine implements CallGraphEngine {
protected final CodebaseContext context;
protected final ConstantResolver constantResolver;
protected final TypeResolver typeResolver;
protected final CallGraphPathFinder pathFinder;
protected final VariableTracer variableTracer;
protected final ConstantExtractor constantExtractor;
protected final ConstructorAnalyzer constructorAnalyzer;
@FunctionalInterface
protected interface RhsResolver {
String resolve(Expression rhs, Map<String, String> paramValues, TypeDeclaration td);
}
public AbstractCallGraphEngine(CodebaseContext context) {
this.context = context;
this.constantResolver = new ConstantResolver();
this.typeResolver = new TypeResolver(context);
this.pathFinder = new CallGraphPathFinder(context);
this.variableTracer = new VariableTracer(context, constantResolver);
this.constantExtractor = new ConstantExtractor(context, constantResolver, this::resolveCalledMethod);
this.constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
// Wire up cross dependencies
this.variableTracer.setConstantExtractor(this.constantExtractor);
this.constantExtractor.setVariableTracer(this.variableTracer);
this.constantExtractor.setConstructorAnalyzer(this.constructorAnalyzer);
this.constructorAnalyzer.setVariableTracer(this.variableTracer);
this.constructorAnalyzer.setConstantExtractor(this.constantExtractor);
}
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
Map<String, List<CallEdge>> callGraph = buildCallGraph();
List<CallChain> chains = new ArrayList<>();
for (EntryPoint ep : entryPoints) {
String startMethod = ep.getClassName() + "." + ep.getMethodName();
boolean foundAny = false;
for (TriggerPoint tp : triggers) {
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
List<String> path = pathFinder.findPath(startMethod, targetMethod, callGraph, new HashSet<>());
if (path != null) {
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 (!foundAny && log.isDebugEnabled()) {
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
}
}
return chains;
}
protected TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
if (path.size() < 2) return tp;
String event = tp.getEvent();
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
String currentParamName = event;
String resolvedValue = event;
String methodSuffix = "";
String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix);
currentParamName = extractedEntry[0];
methodSuffix = extractedEntry[1];
for (int i = path.size() - 1; i > 0; i--) {
String target = path.get(i);
String caller = path.get(i - 1);
int paramIndex = typeResolver.getParameterIndex(target, currentParamName);
if (paramIndex < 0) {
Map<String, String> 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) {
break;
}
List<CallEdge> edges = callGraph.get(caller);
boolean found = false;
if (edges != null) {
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
String expectedType = typeResolver.getParameterType(target, paramIndex);
if (expectedType != null && paramIndex < edge.getArguments().size()) {
String argValue = edge.getArguments().get(paramIndex);
String actualType = null;
if (argValue.contains(".") && !argValue.contains("(")) {
String prefix = argValue.substring(0, argValue.lastIndexOf('.'));
if (!prefix.equals("this") && !prefix.equals("super")) {
actualType = prefix;
}
}
if (actualType == null && !argValue.contains(".")) {
actualType = variableTracer.getVariableDeclaredType(caller, argValue);
}
if (actualType != null && !typeResolver.isTypeCompatible(actualType, expectedType)) {
continue;
}
}
if (paramIndex < edge.getArguments().size()) {
String arg = edge.getArguments().get(paramIndex);
if (arg != null) {
String receiver = edge.getReceiver();
String actualReceiver = null;
if ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super.")) {
if (i - 1 > 0) {
String prevCaller = path.get(i - 2);
List<CallEdge> prevEdges = callGraph.get(prevCaller);
if (prevEdges != null) {
for (CallEdge prevEdge : prevEdges) {
if (prevEdge.getTargetMethod().equals(caller) || pathFinder.isHeuristicMatch(prevEdge.getTargetMethod(), caller)) {
actualReceiver = prevEdge.getReceiver();
break;
}
}
}
}
if (actualReceiver == null) {
actualReceiver = "this";
}
if ("this".equals(arg) || arg.startsWith("this.")) {
arg = arg.replaceFirst("^this", actualReceiver);
}
// Intentionally not replacing 'super' because super should remain statically bound to the superclass of where the code is defined.
}
String[] extractedArg = extractMethodSuffix(arg, methodSuffix);
arg = extractedArg[0];
methodSuffix = extractedArg[1];
currentParamName = arg;
resolvedValue = arg + methodSuffix;
found = true;
break;
}
}
}
}
}
if (!found) break;
}
String entryMethod = path.get(0);
int entryParamIndex = typeResolver.getParameterIndex(entryMethod, currentParamName);
if (entryParamIndex < 0) {
if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) {
String localMethodName = methodSuffix.substring(1).replace("()", "");
Expression localSetterExpr = variableTracer.traceLocalSetter(entryMethod, currentParamName, localMethodName);
if (localSetterExpr != null) {
List<String> setterEvents = new ArrayList<>();
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
for (Expression tracedSetter : tracedSetters) {
constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents);
}
if (!setterEvents.isEmpty()) {
return TriggerPoint.builder()
.event(tp.getEvent())
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(setterEvents)
.build();
}
}
}
boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null
&& !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new ");
if (hasGetterOnReceiver) {
List<String> getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
if (getterEvents != null && !getterEvents.isEmpty()) {
return TriggerPoint.builder()
.event(tp.getEvent())
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(getterEvents)
.build();
}
}
String tracedVar = variableTracer.traceLocalVariable(entryMethod, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
tracedVar = extractedFinalTraced[0];
methodSuffix = extractedFinalTraced[1];
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
}
}
List<String> polymorphicEvents = new ArrayList<>();
if (resolvedValue.startsWith("ENUM_SET:")) {
for (String eVal : resolvedValue.substring(9).split(",")) {
String parsed = constantExtractor.parseEnumSetElement(eVal);
if (!polymorphicEvents.contains(parsed)) polymorphicEvents.add(parsed);
}
return TriggerPoint.builder()
.event(tp.getEvent())
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(polymorphicEvents)
.build();
}
ASTParser exprParser = ASTParser.newParser(AST.JLS17);
exprParser.setKind(ASTParser.K_EXPRESSION);
exprParser.setSource(resolvedValue.toCharArray());
ASTNode exprNode = exprParser.createAST(null);
System.out.println("DEBUG resolvedValue=" + resolvedValue + ", exprNode=" + exprNode.getClass().getSimpleName());
String varName = null;
String methodName = null;
String declaredType = null;
String sourceMethod = null;
if (exprNode instanceof MethodInvocation mi) {
methodName = mi.getName().getIdentifier();
if (mi.getExpression() instanceof SimpleName sn) {
varName = sn.getIdentifier();
} else if (mi.getExpression() instanceof ParenthesizedExpression pe &&
pe.getExpression() instanceof CastExpression ce &&
ce.getExpression() instanceof SimpleName sn) {
varName = sn.getIdentifier();
declaredType = ce.getType().toString();
sourceMethod = "inline-cast";
} else if (mi.getExpression() instanceof ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
if (cic.getAnonymousClassDeclaration() != null) {
final List<String> polyEventsRef = polymorphicEvents;
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
if (declObj instanceof MethodDeclaration mDecl && mDecl.getName().getIdentifier().equals(methodName) && mDecl.getBody() != null) {
mDecl.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement rs) {
if (rs.getExpression() != null) {
List<Expression> tracedReturns = variableTracer.traceVariableAll(rs.getExpression());
for (Expression tracedRet : tracedReturns) {
constantExtractor.extractConstantsFromExpression(tracedRet, polyEventsRef);
}
}
return super.visit(rs);
}
});
}
}
}
} else if (mi.getExpression() instanceof MethodInvocation innerMi) {
methodName = innerMi.getName().getIdentifier();
if (innerMi.getExpression() instanceof SimpleName snInner) {
varName = snInner.getIdentifier();
} else {
String exprStr = innerMi.getExpression() != null ? innerMi.getExpression().toString() : "";
if (!exprStr.contains("(")) {
varName = exprStr;
}
}
} else {
String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "this";
if (!exprStr.contains("(")) {
varName = exprStr;
}
}
} else if (exprNode instanceof ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
} else if (exprNode instanceof ConditionalExpression cond) {
if (cond.getThenExpression() instanceof ClassInstanceCreation cicThen) {
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicThen.getType()));
}
if (cond.getElseExpression() instanceof ClassInstanceCreation cicElse) {
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicElse.getType()));
}
sourceMethod = "inline-ternary";
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
} else if (exprNode instanceof SimpleName sn) {
varName = sn.getIdentifier();
if (varName.equals(varName.toUpperCase()) && varName.contains("_")) {
polymorphicEvents.add(varName);
methodName = null;
} else {
methodName = "VariableReference";
}
} else if (exprNode instanceof ParenthesizedExpression pe &&
pe.getExpression() instanceof CastExpression ce &&
ce.getExpression() instanceof ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
} else if (exprNode instanceof CastExpression ce &&
ce.getExpression() instanceof ClassInstanceCreation cic) {
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
sourceMethod = "inline-instantiation";
polymorphicEvents.add(declaredType);
}
if (methodName != null) {
if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) {
Expression localSetterExpr = variableTracer.traceLocalSetter(entryMethod, varName, methodName);
if (localSetterExpr != null) {
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
for (Expression tracedSetter : tracedSetters) {
constantExtractor.extractConstantsFromExpression(tracedSetter, polymorphicEvents);
if (tracedSetter instanceof ClassInstanceCreation cic && cic.getAnonymousClassDeclaration() != null) {
resolvedValue = cic.toString() + "." + methodName + "()";
}
}
System.out.println("Returning early with events: " + polymorphicEvents + " for " + resolvedValue); if (!polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.sourceModule(tp.getSourceModule())
.stateMachineId(tp.getStateMachineId())
.sourceState(tp.getSourceState())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(polymorphicEvents)
.build();
}
}
}
if (varName != null && declaredType == null) {
if (!varName.isEmpty() && !"this".equals(varName) && !"super".equals(varName)) {
String entryClassName = entryMethod.substring(0, entryMethod.lastIndexOf('.'));
String entryMethodName = entryMethod.substring(entryMethod.lastIndexOf('.') + 1);
TypeDeclaration currentTd = context.getTypeDeclaration(entryClassName);
if (currentTd != null) {
MethodDeclaration currentMd = context.findMethodDeclaration(currentTd, entryMethodName, true);
if (currentMd != null) {
final String[] extractedFinalTraced = {null, null};
final String targetVar = varName;
final String mName = methodName;
final String[] finalDeclaredType = {null};
final String[] finalSourceMethod = {null};
currentMd.accept(new ASTVisitor() {
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
if (fragObj instanceof VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(targetVar)) {
if (vdf.getInitializer() != null) {
Expression valueNode = variableTracer.traceVariable(vdf.getInitializer());
if (valueNode instanceof ClassInstanceCreation cic) {
if (cic.getAnonymousClassDeclaration() != null) {
extractedFinalTraced[0] = cic.toString();
extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
} else {
finalDeclaredType[0] = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
finalSourceMethod[0] = "inline-instantiation";
}
} else {
extractedFinalTraced[0] = valueNode.toString();
extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : "";
}
}
}
}
return super.visit(node);
}
});
if (finalDeclaredType[0] != null) {
declaredType = finalDeclaredType[0];
sourceMethod = finalSourceMethod[0];
}
if (extractedFinalTraced[0] != null) {
resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1];
if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) {
ASTParser p = ASTParser.newParser(AST.JLS17);
p.setKind(ASTParser.K_EXPRESSION);
p.setSource(resolvedValue.toCharArray());
ASTNode newNode = p.createAST(null);
if (newNode instanceof Expression) {
List<Expression> traced = variableTracer.traceVariableAll((Expression) newNode);
for (Expression ex : traced) {
constantExtractor.extractConstantsFromExpression(ex, polymorphicEvents);
}
}
}
}
}
}
}
if (declaredType == null) {
for (String methodFqn : path) {
declaredType = variableTracer.getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
List<String> values = constantExtractor.resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu);
System.out.println("RESOLVED VALUES FOR " + methodName + " in " + currentTd.getName() + ": " + values); if (values != null && !values.isEmpty()) {
polymorphicEvents.addAll(values);
}
if (polymorphicEvents.isEmpty() && variableTracer != null) {
String initializer = variableTracer.traceLocalVariable(methodFqn, varName);
if (initializer != null && !initializer.equals(varName)) {
ASTParser mapParser = ASTParser.newParser(AST.JLS17);
mapParser.setKind(ASTParser.K_EXPRESSION);
mapParser.setSource(initializer.toCharArray());
ASTNode mapNode = mapParser.createAST(null);
if (mapNode instanceof Expression) {
List<Expression> mapTraced = variableTracer.traceVariableAll((Expression) mapNode);
for (Expression t : mapTraced) {
constantExtractor.extractConstantsFromExpression(t, polymorphicEvents);
}
}
}
}
sourceMethod = methodFqn;
break;
}
}
if (declaredType == null && varName.matches("^[A-Z].*")) {
TypeDeclaration staticTd = context.getTypeDeclaration(varName);
if (staticTd != null) {
declaredType = context.getFqn(staticTd);
sourceMethod = "static-call";
} else if (context.getEnumValues(varName) != null) {
declaredType = varName;
sourceMethod = "static-call";
}
}
}
}
if (declaredType == null && methodName != null && !methodName.equals("VariableReference")) {
for (String methodFqn : path) {
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
if (currentTd != null && context.findMethodDeclaration(currentTd, methodName, true) != null) {
CompilationUnit cu = currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
List<String> values = constantExtractor.resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu);
System.out.println("DEBUG RESOLVED VALUES FOR " + methodName + " in " + currentTd.getName() + " -> " + values);
if (values != null && !values.isEmpty()) {
polymorphicEvents.addAll(values);
sourceMethod = methodFqn;
break;
}
}
}
}
if (declaredType != null) {
List<String> enumValues = context.getEnumValues(declaredType);
if (enumValues != null && !enumValues.isEmpty()) {
for (String ev : enumValues) {
polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev));
}
} else {
List<String> typesToInspect = new ArrayList<>();
if ("inline-instantiation".equals(sourceMethod)) {
typesToInspect.add(declaredType);
} else {
typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType));
}
for (String type : typesToInspect) {
Set<String> visited = new HashSet<>();
TypeDeclaration baseTd = context.getTypeDeclaration(type);
CompilationUnit cuToUse = null;
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) baseTd.getRoot();
} else {
String firstPathMethod = path.get(0);
String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.'));
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) entryTd.getRoot();
}
}
List<String> constants = constantExtractor.resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
for (String constant : constants) {
if (!polymorphicEvents.contains(constant)) {
polymorphicEvents.add(constant);
}
}
}
}
}
}
boolean hasValidConstant = false;
for (String ev : polymorphicEvents) {
String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev;
if (val.equals(val.toUpperCase()) && val.length() > 2) {
hasValidConstant = true;
break;
}
}
if (!hasValidConstant && exprNode instanceof Expression) {
constantExtractor.extractConstantsFromExpression((Expression) exprNode, polymorphicEvents);
}
List<String> newPolyEvents = new ArrayList<>();
for (String pe : polymorphicEvents) {
List<String> resolved = constantExtractor.resolveClassConstantReturns(pe, context, null);
if (resolved != null && !resolved.isEmpty()) {
newPolyEvents.addAll(resolved);
} else {
newPolyEvents.add(pe);
}
}
polymorphicEvents = newPolyEvents;
polymorphicEvents.removeIf(e -> {
if (e.contains(".")) return false;
String val = e;
boolean isKnownEnumVal = false;
for (List<String> vals : context.getEnumValuesMap().values()) {
for (String v : vals) {
if (v.endsWith("." + val)) {
isKnownEnumVal = true;
break;
}
}
if (isKnownEnumVal) break;
}
if (isKnownEnumVal) return false;
return !val.equals(val.toUpperCase()) || val.length() <= 1;
});
String targetMethod = path.get(path.size() - 1);
int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event);
if (eventParamIndex < 0) {
eventParamIndex = 0;
}
String expectedType = typeResolver.getParameterType(targetMethod, eventParamIndex);
if (expectedType != null) {
final String expType = expectedType;
boolean isExpectedEnum = context.getEnumValues(expType) != null;
polymorphicEvents.removeIf(e -> {
if (e.contains(".")) {
String actualType = e.substring(0, e.lastIndexOf('.'));
boolean isActualEnum = context.getEnumValues(actualType) != null;
if (isExpectedEnum && isActualEnum) {
return !typeResolver.isTypeCompatible(actualType, expType);
}
}
return false;
});
}
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(polymorphicEvents)
.build();
}
return tp;
}
protected MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
return (MethodDeclaration) parent;
}
protected TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
protected List<String> resolveArguments(List<?> astArguments) {
List<String> args = new ArrayList<>();
for (Object argObj : astArguments) {
args.add(resolveArgument((Expression) argObj));
}
return args;
}
protected String resolveArgument(Expression expr) {
if (expr == null) {
return "null";
}
if (expr instanceof LambdaExpression le) {
ASTNode body = le.getBody();
if (body instanceof Expression bodyExpr) {
expr = bodyExpr;
} else if (body instanceof Block block) {
for (Object stmtObj : block.statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
if (expr instanceof ExpressionMethodReference emr) {
TypeDeclaration td = findEnclosingType(emr);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
if (md != null && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
}
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
Expression firstArg = (Expression) cic.arguments().get(0);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg;
}
}
String val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString();
}
System.out.println("DEBUG resolveArgument expr=" + expr + ", val=" + val); return postProcessResolvedArgument(expr, val);
}
protected String postProcessResolvedArgument(Expression originalExpr, String resolvedValue) {
return resolvedValue;
}
protected String resolveMethodInType(TypeDeclaration td, String methodName) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
TypeDeclaration declaringTd = findEnclosingType(md);
return context.getFqn(declaringTd) + "." + methodName;
}
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
TypeDeclaration targetTd = context.resolveStaticImport(methodName, cu);
if (targetTd != null) {
return context.getFqn(targetTd) + "." + methodName;
}
return null;
}
protected String resolveMethodInTypeHierarchy(TypeDeclaration td, String methodName) {
String resolved = resolveMethodInType(td, methodName);
if (resolved != null) {
return resolved;
}
String superFqn = context.getSuperclassFqn(td);
while (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
resolved = resolveMethodInType(superTd, methodName);
if (resolved != null) {
return resolved;
}
superFqn = context.getSuperclassFqn(superTd);
} else {
break;
}
}
return null;
}
protected Expression traceVariable(Expression expr) {
return variableTracer.traceVariable(expr);
}
protected Map<String, String> buildFieldValuesFromCIC(
TypeDeclaration td,
ClassInstanceCreation cic,
CodebaseContext context,
RhsResolver rhsResolver) {
return constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, (rhs, paramValues, typeDecl) -> rhsResolver.resolve(rhs, paramValues, typeDecl));
}
protected Map<String, String> buildFieldValuesFromCIC(
TypeDeclaration td,
ClassInstanceCreation cic,
CodebaseContext context) {
return constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context);
}
protected String evaluateMethodCallInConstructor(
Expression rhs,
Map<String, String> paramValues,
TypeDeclaration td) {
return constructorAnalyzer.evaluateMethodCallInConstructor(rhs, paramValues, td);
}
protected ClassInstanceCreation findReturnedCIC(
String className, String methodName, CodebaseContext context) {
return constructorAnalyzer.findReturnedCIC(className, methodName, context);
}
protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
ASTParser exprParser = ASTParser.newParser(AST.JLS17);
exprParser.setKind(ASTParser.K_EXPRESSION);
exprParser.setSource(resolvedValue.toCharArray());
ASTNode exprNode = exprParser.createAST(null);
if (exprNode instanceof SuperMethodInvocation smi) {
String getterName = smi.getName().getIdentifier();
int lastDot = entryMethod.lastIndexOf('.');
String className = lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
TypeDeclaration currentTd = context.getTypeDeclaration(className);
if (currentTd != null) {
String receiverType = context.getSuperclassFqn(currentTd);
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), null);
}
} else if (exprNode instanceof MethodInvocation mi) {
String getterName = mi.getName().getIdentifier();
String propName = methodSuffix.startsWith(".get") ? methodSuffix.substring(4) : methodSuffix.substring(1);
Expression receiver = mi.getExpression();
String receiverName = null;
String receiverType = null;
if (receiver instanceof SimpleName sn) {
receiverName = sn.getIdentifier();
receiverType = variableTracer.getVariableDeclaredType(entryMethod, receiverName);
} else if (receiver instanceof MethodInvocation receiverMi) {
Expression currentMi = receiverMi;
while (currentMi instanceof MethodInvocation chainMi) {
String mName = chainMi.getName().getIdentifier();
if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
if (!chainMi.arguments().isEmpty()) {
Expression arg = (Expression) chainMi.arguments().get(0);
List<String> builderConstants = new ArrayList<>();
constantExtractor.extractConstantsFromExpression(arg, builderConstants);
if (!builderConstants.isEmpty()) {
return builderConstants;
}
}
}
currentMi = chainMi.getExpression();
}
} else if (receiver instanceof SuperMethodInvocation) {
int lastDot = entryMethod.lastIndexOf('.');
String className = lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
TypeDeclaration currentTd = context.getTypeDeclaration(className);
if (currentTd != null) {
receiverType = context.getSuperclassFqn(currentTd);
}
}
if (receiverType == null && path.size() > 1) {
String callerMethod = path.get(1);
receiverType = variableTracer.getVariableDeclaredType(callerMethod, receiverName);
}
if (receiverType != null) {
String simpleReceiverType = receiverType;
if (receiverType.contains("<")) {
String rawType = receiverType.substring(0, receiverType.indexOf('<'));
if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || context.getTypeDeclaration(rawType) == null) {
int start = receiverType.indexOf('<');
int end = receiverType.lastIndexOf('>');
if (start >= 0 && end > start) {
simpleReceiverType = receiverType.substring(start + 1, end);
}
} else {
simpleReceiverType = rawType;
}
}
TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType);
if (td != null) {
Map<String, String> fieldValues = new HashMap<>();
String varName = receiverName;
Expression initExpr = findVariableInitializer(entryMethod, varName);
if (initExpr instanceof MethodInvocation initMi) {
initExpr = unwrapMethodInvocation(initMi, 0);
}
if (!(initExpr instanceof ClassInstanceCreation cic)) {
return null;
}
MethodDeclaration getter = null;
if (cic.getAnonymousClassDeclaration() != null) {
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) {
getter = md;
break;
}
}
}
if (getter == null) {
getter = context.findMethodDeclaration(td, getterName, true);
}
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);
String methodName = entryMethod.substring(lastDot + 1);
TypeDeclaration entryTd = context.getTypeDeclaration(className);
if (entryTd != null) {
MethodDeclaration entryMd = context.findMethodDeclaration(entryTd, methodName, false);
if (entryMd != null && entryMd.getBody() != null) {
// Skip trackSetterCallsBetween
}
}
}
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
if (result != null) {
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
if (returnType != null && !result.contains(".")) {
int lastDotRet = returnType.lastIndexOf('.');
String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
if (!simpleReturnType.equals("String") && !simpleReturnType.equals("Integer") && !simpleReturnType.equals("Long") && !simpleReturnType.equals("Boolean")) {
result = simpleReturnType + "." + result;
}
}
return Collections.singletonList(result);
}
}
}
}
}
return null;
}
protected Expression findVariableInitializer(String methodFqn, String varName) {
int lastDot = methodFqn.lastIndexOf('.');
if (lastDot < 0) return null;
String className = methodFqn.substring(0, lastDot);
String methodName = methodFqn.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) return null;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
if (md == null || md.getBody() == null) return null;
final Expression[] initExpr = {null};
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName) && frag.getInitializer() != null) {
initExpr[0] = frag.getInitializer();
return false;
}
}
return super.visit(node);
}
});
return initExpr[0];
}
protected String[] extractMethodSuffix(String paramName, String currentSuffix) {
if (paramName != null && !paramName.contains(" ") && !paramName.startsWith("new ")) {
int bracketIndex = paramName.indexOf('[');
int dotIndex = paramName.indexOf('.');
if (bracketIndex > 0 && (dotIndex == -1 || bracketIndex < dotIndex)) {
return new String[] { paramName.substring(0, bracketIndex), paramName.substring(bracketIndex) + currentSuffix };
} else if (dotIndex > 0) {
if (paramName.startsWith("this.")) {
int nextDotIndex = paramName.indexOf('.', 5);
if (nextDotIndex > 0) {
return new String[] { paramName.substring(0, nextDotIndex), paramName.substring(nextDotIndex) + currentSuffix };
}
return new String[] { paramName, currentSuffix };
}
return new String[] { paramName.substring(0, dotIndex), paramName.substring(dotIndex) + currentSuffix };
}
}
return new String[] { paramName, currentSuffix };
}
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
if (depth > 5) return mi;
if (mi.getExpression() != null) {
String exprStr = mi.getExpression().toString();
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) {
return mi;
}
}
if (!mi.arguments().isEmpty()) {
String mName = mi.getName().getIdentifier();
String lowerName = mName.toLowerCase();
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
lowerName.startsWith("create") || lowerName.startsWith("from") ||
lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
lowerName.startsWith("extract") || lowerName.startsWith("get") ||
lowerName.startsWith("find")) {
return mi;
}
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof MethodInvocation innerMi) {
return unwrapMethodInvocation(innerMi, depth + 1);
}
return arg;
}
return mi;
}
public List<String> resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) {
return constantExtractor.resolveClassConstantReturns(className, context, contextCu);
}
protected abstract Map<String, List<CallEdge>> buildCallGraph();
protected abstract String resolveCalledMethod(MethodInvocation node);
}

View File

@@ -1,788 +0,0 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
@Slf4j
public class CallGraphBuilder {
private final CodebaseContext context;
private final ConstantResolver constantResolver;
private String currentMethodFqn;
private Map<String, List<CallEdge>> graph;
@lombok.Data
private static class CallEdge {
private final String targetMethod;
private final List<String> arguments;
}
public CallGraphBuilder(CodebaseContext context) {
this.context = context;
this.constantResolver = new ConstantResolver();
}
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
Map<String, List<CallEdge>> callGraph = buildCallGraph();
List<CallChain> chains = new ArrayList<>();
for (EntryPoint ep : entryPoints) {
String startMethod = ep.getClassName() + "." + ep.getMethodName();
boolean foundAny = false;
for (TriggerPoint tp : triggers) {
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
if (path != null) {
foundAny = true;
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
String contextMachineId = extractContextMachineId(path, callGraph);
chains.add(CallChain.builder()
.entryPoint(ep)
.triggerPoint(resolvedTp)
.methodChain(path)
.contextMachineId(contextMachineId)
.build());
}
}
if (!foundAny && log.isDebugEnabled()) {
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
}
}
return chains;
}
private TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
if (path.size() < 2) return tp;
String event = tp.getEvent();
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
String currentParamName = event;
String resolvedValue = event;
String methodSuffix = "";
// Extract method calls like .getType() so we can trace the base parameter
int dotIndex = currentParamName.indexOf('.');
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) {
char nextChar = currentParamName.charAt(dotIndex + 1);
if (Character.isLowerCase(nextChar)) {
methodSuffix = currentParamName.substring(dotIndex);
currentParamName = currentParamName.substring(0, dotIndex);
}
}
// Walk backwards up the call chain
for (int i = path.size() - 1; i > 0; i--) {
String target = path.get(i);
String caller = path.get(i - 1);
// Find parameter index in target method
int paramIndex = getParameterIndex(target, currentParamName);
if (paramIndex < 0) {
// Not a parameter. Maybe it's a local variable initialized from a parameter or method call?
String tracedVar = traceLocalVariable(target, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
// Extract method calls like .getType() from the traced variable
int dotIdx = tracedVar.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
paramIndex = getParameterIndex(target, currentParamName);
}
}
if (paramIndex < 0) {
break; // Parameter name changed or not found, stop tracing
}
// Find the edge from caller to target to get the argument passed
List<CallEdge> edges = callGraph.get(caller);
boolean found = false;
if (edges != null) {
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
if (paramIndex < edge.getArguments().size()) {
String arg = edge.getArguments().get(paramIndex);
if (arg != null) {
// If the argument passed has a method call, extract it
int dotIdx = arg.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) {
methodSuffix = arg.substring(dotIdx) + methodSuffix;
arg = arg.substring(0, dotIdx);
}
currentParamName = arg;
resolvedValue = arg + methodSuffix;
found = true;
break;
}
}
}
}
}
if (!found) break; // Could not map argument
}
// Final check on the entry method
String entryMethod = path.get(0);
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
if (entryParamIndex < 0) {
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
int dotIdx = tracedVar.indexOf('.');
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
tracedVar = tracedVar.substring(0, dotIdx);
}
currentParamName = tracedVar;
resolvedValue = tracedVar + methodSuffix;
}
}
List<String> polymorphicEvents = new ArrayList<>();
if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) {
int lastDot = resolvedValue.lastIndexOf('.');
int firstDot = resolvedValue.indexOf('.');
int openParen = resolvedValue.indexOf('(', lastDot);
if (lastDot > 0 && openParen > lastDot) {
String varName = resolvedValue.substring(0, firstDot);
if (varName.contains("(")) {
varName = null;
}
String methodName = resolvedValue.substring(lastDot + 1, openParen);
if (varName != null) {
// Resolve in the first method in the path where the variable might be declared
for (String methodFqn : path) {
String declaredType = getVariableDeclaredType(methodFqn, varName);
if (declaredType != null) {
System.out.println("DEEP TRACE: " + methodFqn + " " + varName + " -> " + declaredType);
List<String> typesToInspect = new ArrayList<>();
typesToInspect.add(declaredType);
typesToInspect.addAll(context.getImplementations(declaredType));
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
for (String type : typesToInspect) {
Set<String> visited = new HashSet<>();
// We must find the compilation unit to pass for simple names!
// Let's pass the first available CU for this class
TypeDeclaration baseTd = context.getTypeDeclaration(type);
CompilationUnit cuToUse = null;
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) baseTd.getRoot();
} else {
String entryClassName = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
cuToUse = (CompilationUnit) entryTd.getRoot();
}
}
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants);
for (String constant : constants) {
if (!polymorphicEvents.contains(constant)) {
polymorphicEvents.add(constant);
}
}
}
break;
}
}
}
}
}
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
return TriggerPoint.builder()
.event(resolvedValue)
.className(tp.getClassName())
.methodName(tp.getMethodName())
.sourceFile(tp.getSourceFile())
.lineNumber(tp.getLineNumber())
.polymorphicEvents(polymorphicEvents)
.build();
}
return tp;
}
private int getParameterIndex(String methodFqn, String paramName) {
if (methodFqn == null || !methodFqn.contains(".")) return -1;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
for (int i = 0; i < md.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
if (svd.getName().getIdentifier().equals(paramName)) {
return i;
}
}
}
}
return -1;
}
private String getVariableDeclaredType(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
for (Object pObj : md.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
if (svd.getName().getIdentifier().equals(varName)) {
return svd.getType().toString();
}
}
final String[] foundType = new String[1];
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType().toString();
}
}
return super.visit(node);
}
});
}
return foundType[0];
}
}
return null;
}
private List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
if (depth > 20) return Collections.emptyList();
String fqn = className + "." + methodName;
if (!visited.add(fqn)) return Collections.emptyList();
List<String> constants = new ArrayList<>();
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
if (td == null) {
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
}
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
Expression retExpr = node.getExpression();
if (retExpr != null) {
boolean handled = false;
if (retExpr instanceof MethodInvocation mi) {
// Follow delegation first
String called = resolveCalledMethod(mi);
System.out.println("DEEP TRACE RESOLVED CALLED: " + called);
if (called != null && called.contains(".")) {
if (visited.contains(called)) {
handled = true;
} else {
String cName = called.substring(0, called.lastIndexOf('.'));
String mName = called.substring(called.lastIndexOf('.') + 1);
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
}
if (!handled) {
String val = constantResolver.resolve(retExpr, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
constants.add(eVal.substring(eVal.lastIndexOf('.') + 1));
}
} else {
constants.add(val);
}
} else if (retExpr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (retExpr instanceof SimpleName sn) {
constants.add(sn.toString());
}
}
}
return super.visit(node);
}
});
}
}
visited.remove(fqn);
return constants;
}
private String traceLocalVariable(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
final Expression[] initializer = new Expression[1];
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializer[0] = node.getInitializer();
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
initializer[0] = node.getRightHandSide();
}
return super.visit(node);
}
});
if (initializer[0] != null) {
Expression expr = traceVariable(initializer[0]);
if (expr instanceof MethodInvocation mi) {
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
Expression innerMost = unwrapMethodInvocation(mi, 0);
if (innerMost instanceof MethodInvocation innerMi) {
if (innerMi.getExpression() instanceof SimpleName sn) {
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
}
return innerMi.getName().getIdentifier() + "()";
}
if (innerMost instanceof SimpleName sn) {
return sn.getIdentifier();
}
return innerMost.toString();
}
if (expr instanceof SimpleName sn) {
return sn.getIdentifier();
}
return expr.toString();
}
}
}
return null;
}
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
if (depth > 5) return mi;
if (!mi.arguments().isEmpty()) {
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof MethodInvocation innerMi) {
return unwrapMethodInvocation(innerMi, depth + 1);
}
return arg;
}
return mi;
}
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
for (String node : path) {
List<CallEdge> edges = callGraph.get(node);
if (edges != null) {
for (CallEdge edge : edges) {
String target = edge.getTargetMethod();
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
// Persister signatures usually like: restore(stateMachine, contextObj)
if (edge.getArguments().size() >= 2) {
return edge.getArguments().get(1); // The contextObj / machineId
}
}
}
}
}
return null;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
return (MethodDeclaration) parent;
}
private Map<String, List<CallEdge>> buildCallGraph() {
graph = new HashMap<>();
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration td = findEnclosingType(md);
if (td != null) {
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
List<String> args = resolveArguments(node.arguments());
for (String calledMethod : calledMethods) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) {
String typeName = emr.getExpression().toString();
if ("this".equals(typeName) || "super".equals(typeName)) {
TypeDeclaration td2 = findEnclosingType(node);
if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
}
}
} else {
String fallbackTypeFqn = null;
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
if (binding != null) {
fallbackTypeFqn = binding.getQualifiedName();
} else if (emr.getExpression() instanceof SimpleName sn) {
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
}
if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
}
}
}
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperMethodInvocation node) {
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration tdOuter = findEnclosingType(md);
if (tdOuter != null) {
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
String methodName = node.getName().getIdentifier();
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
String calledMethod = null;
if (superTd != null) {
calledMethod = resolveMethodInType(superTd, methodName);
}
if (calledMethod == null) {
calledMethod = superFqn + "." + methodName;
}
List<String> args = resolveArguments(node.arguments());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
}
}
}
return super.visit(node);
}
});
}
return graph;
}
private List<String> resolveArguments(List<?> astArguments) {
List<String> args = new ArrayList<>();
for (Object argObj : astArguments) {
Expression expr = (Expression) argObj;
// Extract from lambda
if (expr instanceof LambdaExpression le) {
ASTNode body = le.getBody();
if (body instanceof Expression bodyExpr) {
expr = bodyExpr;
} else if (body instanceof Block block) {
for (Object stmtObj : block.statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
if (expr instanceof ExpressionMethodReference emr) {
TypeDeclaration td = findEnclosingType(expr);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
if (md != null && md.getBody() != null) {
for (Object stmtObj : md.getBody().statements()) {
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
expr = rs.getExpression();
break;
}
}
}
}
}
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
Expression tracedExpr = traceVariable(expr);
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
}
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
Expression firstArg = (Expression) cic.arguments().get(0);
String resolved = constantResolver.resolve(firstArg, context);
if (resolved != null) {
expr = firstArg; // Only unwrap if it's actually a constant
}
}
String val = constantResolver.resolve(expr, context);
if (val == null) {
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
}
args.add(val);
}
return args;
}
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
String baseCalled = resolveCalledMethod(node);
if (baseCalled == null) return Collections.emptyList();
List<String> allResolved = new ArrayList<>();
allResolved.add(baseCalled);
if (baseCalled.contains(".")) {
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
List<String> impls = context.getImplementations(className);
for (String impl : impls) {
allResolved.add(impl + "." + methodName);
}
}
return allResolved;
}
private String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier();
if (receiver == null) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return resolveMethodInType(td, methodName);
}
return null;
}
ITypeBinding binding = receiver.resolveTypeBinding();
if (binding != null) {
return binding.getQualifiedName() + "." + methodName;
}
if (receiver instanceof ThisExpression) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return context.getFqn(td) + "." + methodName;
}
}
if (receiver instanceof SimpleName sn) {
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
if (fallbackTypeFqn != null) {
return fallbackTypeFqn + "." + methodName;
}
String receiverName = sn.getIdentifier();
return receiverName + "." + methodName;
}
return null;
}
private String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
String varName = receiverNameNode.getIdentifier();
// 1. Check local variables in enclosing method
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
if (enclosingMethod != null) {
// Check parameters
for (Object paramObj : enclosingMethod.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
if (svd.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(svd.getType(), receiverNameNode);
}
}
// Check method body (local variables)
if (enclosingMethod.getBody() != null) {
Type[] foundType = new Type[1];
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType();
}
}
return super.visit(node);
}
});
if (foundType[0] != null) {
return resolveTypeToFqn(foundType[0], receiverNameNode);
}
}
}
// 2. Check fields in enclosing class
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
if (enclosingType != null) {
for (FieldDeclaration field : enclosingType.getFields()) {
for (Object fragObj : field.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(field.getType(), receiverNameNode);
}
}
}
}
return null;
}
private String resolveTypeToFqn(Type type, ASTNode contextNode) {
if (type == null) return null;
String simpleName;
if (type.isSimpleType()) {
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
} else {
simpleName = type.toString();
}
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
return simpleName;
}
private String resolveMethodInType(TypeDeclaration td, String methodName) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
TypeDeclaration declaringTd = findEnclosingType(md);
return context.getFqn(declaringTd) + "." + methodName;
}
return null;
}
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
if (start.equals(target)) return new ArrayList<>(List.of(start));
if (!visited.add(start)) return null; // Path-scoped cycle detection
List<CallEdge> neighbors = graph.get(start);
if (neighbors != null) {
for (CallEdge edge : neighbors) {
String neighbor = edge.getTargetMethod();
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
visited.remove(start);
return new ArrayList<>(List.of(start, target));
}
List<String> path = findPath(neighbor, target, graph, visited);
if (path != null) {
path.add(0, start);
visited.remove(start);
return path;
}
}
}
if (log.isDebugEnabled()) {
log.debug("Path search dead-end at {} when looking for {}", start, target);
}
visited.remove(start);
return null;
}
private boolean isHeuristicMatch(String neighbor, String target) {
if (target.endsWith("." + neighbor)) return true;
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
return simpleNeighbor.equals(simpleTarget);
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
private Expression traceVariable(Expression expr) {
if (expr instanceof SimpleName sn) {
String varName = sn.getIdentifier();
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
if (enclosingMethod != null) {
final Expression[] initializer = new Expression[1];
enclosingMethod.accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializer[0] = node.getInitializer();
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
initializer[0] = node.getRightHandSide();
}
return super.visit(node);
}
});
if (initializer[0] != null) {
return traceVariable(initializer[0]);
}
}
}
return expr;
}
}

View File

@@ -0,0 +1,11 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import java.util.List;
public interface CallGraphEngine {
List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers);
}

View File

@@ -0,0 +1,118 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
@Slf4j
public class CallGraphPathFinder {
private final CodebaseContext context;
public CallGraphPathFinder(CodebaseContext context) {
this.context = context;
}
public List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
if (start.equals(target)) return new ArrayList<>(List.of(start));
if (!visited.add(start)) return null; // Path-scoped cycle detection
List<CallEdge> neighbors = graph.get(start);
if (neighbors != null) {
for (CallEdge edge : neighbors) {
String neighbor = edge.getTargetMethod();
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
visited.remove(start);
return new ArrayList<>(List.of(start, target));
}
List<String> path = findPath(neighbor, target, graph, visited);
if (path != null) {
path.add(0, start);
visited.remove(start);
return path;
}
}
}
if (log.isDebugEnabled()) {
log.debug("Path search dead-end at {} when looking for {}", start, target);
}
visited.remove(start);
return null;
}
public String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
for (String node : path) {
List<CallEdge> edges = callGraph.get(node);
if (edges != null) {
for (CallEdge edge : edges) {
String target = edge.getTargetMethod();
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
if (edge.getArguments().size() >= 2) {
return edge.getArguments().get(1); // The contextObj / machineId
}
}
}
}
}
return null;
}
public boolean isHeuristicMatch(String neighbor, String target) {
if (neighbor == null || target == null) return false;
if (neighbor.equals(target)) return true;
if (isFullyQualifiedMethod(neighbor) && isFullyQualifiedMethod(target)) {
int lastDotNeighbor = neighbor.lastIndexOf('.');
int lastDotTarget = target.lastIndexOf('.');
if (lastDotNeighbor < 0 || lastDotTarget < 0) return false;
String methodNeighbor = neighbor.substring(lastDotNeighbor + 1);
String methodTarget = target.substring(lastDotTarget + 1);
if (!methodNeighbor.equals(methodTarget)) {
return false;
}
String classNeighbor = neighbor.substring(0, lastDotNeighbor);
String classTarget = target.substring(0, lastDotTarget);
if (classNeighbor.equals(classTarget)) return true;
String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
if (simpleClassNeighbor.contains(simpleClassTarget) || simpleClassTarget.contains(simpleClassNeighbor)) return true;
List<String> impls = context.getImplementations(classTarget);
if (impls != null) {
for (String impl : impls) {
if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true;
}
}
List<String> implsN = context.getImplementations(classNeighbor);
if (implsN != null) {
for (String impl : implsN) {
if (impl.equals(classTarget) || impl.endsWith("." + classTarget)) return true;
}
}
return false;
}
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
return simpleNeighbor.equals(simpleTarget);
}
private boolean isFullyQualifiedMethod(String methodFqn) {
if (methodFqn == null || !methodFqn.contains(".")) return false;
int lastDot = methodFqn.lastIndexOf('.');
String classPart = methodFqn.substring(0, lastDot);
if (classPart.contains(".")) return true;
if (!classPart.isEmpty() && Character.isUpperCase(classPart.charAt(0))) return true;
return false;
}
}

View File

@@ -0,0 +1,383 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
import java.util.function.Function;
public class ConstantExtractor {
private final CodebaseContext context;
private final ConstantResolver constantResolver;
private final Function<MethodInvocation, String> calledMethodResolver;
private VariableTracer variableTracer;
private ConstructorAnalyzer constructorAnalyzer;
public ConstantExtractor(CodebaseContext context, ConstantResolver constantResolver, Function<MethodInvocation, String> calledMethodResolver) {
this.context = context;
this.constantResolver = constantResolver;
this.calledMethodResolver = calledMethodResolver;
}
public void setVariableTracer(VariableTracer variableTracer) {
this.variableTracer = variableTracer;
}
public void setConstructorAnalyzer(ConstructorAnalyzer constructorAnalyzer) {
this.constructorAnalyzer = constructorAnalyzer;
}
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
if (expr instanceof QualifiedName qn) {
constants.add(qn.toString());
} else if (expr instanceof StringLiteral sl) {
constants.add(sl.getLiteralValue());
} else if (expr instanceof ConditionalExpression ce) {
extractConstantsFromExpression(ce.getThenExpression(), constants);
extractConstantsFromExpression(ce.getElseExpression(), constants);
} else if (expr instanceof ParenthesizedExpression pe) {
extractConstantsFromExpression(pe.getExpression(), constants);
} else if (expr instanceof CastExpression ce) {
extractConstantsFromExpression(ce.getExpression(), constants);
} else if (expr instanceof Assignment assignment) {
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
} else if (expr instanceof SwitchExpression se) {
se.accept(new ASTVisitor() {
@Override
public boolean visit(YieldStatement ys) {
extractConstantsFromExpression(ys.getExpression(), constants);
return super.visit(ys);
}
@Override
public boolean visit(ExpressionStatement es) {
extractConstantsFromExpression(es.getExpression(), constants);
return super.visit(es);
}
@Override
public boolean visit(ReturnStatement rs) {
extractConstantsFromExpression(rs.getExpression(), constants);
return super.visit(rs);
}
});
} else if (expr instanceof ArrayAccess aa) {
extractConstantsFromExpression(aa.getArray(), constants);
} else if (expr instanceof ArrayCreation ac && ac.getInitializer() != null) {
for (Object expObj : ac.getInitializer().expressions()) {
extractConstantsFromArgument((Expression) expObj, constants);
}
} else if (expr instanceof ArrayInitializer ai) {
for (Object expObj : ai.expressions()) {
extractConstantsFromArgument((Expression) expObj, constants);
}
} else if (expr instanceof MethodInvocation mi) {
String methodName = mi.getName().getIdentifier();
System.out.println("Processing mi: " + mi); if (methodName.equals("get") || methodName.equals("getOrDefault")) {
if (mi.getExpression() != null) {
if (variableTracer != null) {
List<Expression> traced = variableTracer.traceVariableAll(mi.getExpression());
for (Expression t : traced) {
extractConstantsFromExpression(t, constants);
}
} else {
extractConstantsFromExpression(mi.getExpression(), constants);
}
}
return;
}
if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry")) {
for (Object argObj : mi.arguments()) {
extractConstantsFromExpression((Expression) argObj, constants);
}
return;
}
if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof SimpleName sn) {
String varName = sn.getIdentifier();
String propName = methodName.startsWith("get") ? methodName.substring(3) : methodName;
Block block = findEnclosingBlock(mi);
if (block != null) {
for (Object stmtObj : block.statements()) {
if (stmtObj == mi.getParent() || stmtObj == mi) break;
if (stmtObj instanceof ExpressionStatement es) {
if (es.getExpression() instanceof MethodInvocation setterMi) {
if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) {
if (setterMi.getExpression() instanceof SimpleName setterSn && setterSn.getIdentifier().equals(varName)) {
if (!setterMi.arguments().isEmpty()) {
extractConstantsFromExpression((Expression) setterMi.arguments().get(0), constants);
return;
}
}
}
} else if (es.getExpression() instanceof Assignment assignment) {
if (assignment.getLeftHandSide() instanceof FieldAccess fa) {
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
return;
}
}
} else if (assignment.getLeftHandSide() instanceof QualifiedName qqn) {
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
return;
}
}
}
}
}
}
}
}
if (mi.getExpression() instanceof ClassInstanceCreation cic) {
if (cic.getAnonymousClassDeclaration() != null) {
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
if (declObj instanceof MethodDeclaration mdecl) {
if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) {
mdecl.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement rs) {
if (rs.getExpression() != null) {
extractConstantsFromExpression(rs.getExpression(), constants);
}
return super.visit(rs);
}
});
}
}
}
} else if (constructorAnalyzer != null) {
boolean handledByLombok = false;
if (methodName.startsWith("get") && methodName.length() > 3) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
TypeDeclaration typeDecl = context.getTypeDeclaration(typeName);
if (typeDecl != null) {
String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
int fieldIndex = constructorAnalyzer.findConstructorArgumentIndexForLombokField(typeDecl, fieldName);
if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) {
extractConstantsFromArgument((Expression) cic.arguments().get(fieldIndex), constants);
handledByLombok = true;
}
}
}
if (!handledByLombok && methodName != null && !methodName.isEmpty()) {
List<String> narrowed = constructorAnalyzer.resolveInlineInstantiationGetterResult(
cic, methodName, context, new HashSet<>());
if (narrowed.isEmpty()) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
CompilationUnit contextCu = expr.getRoot() instanceof CompilationUnit ? (CompilationUnit) expr.getRoot() : null;
TypeDeclaration typeDecl = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName);
if (typeDecl == null) typeDecl = context.getTypeDeclaration(typeName);
if (typeDecl != null) {
narrowed = resolveMethodReturnConstant(context.getFqn(typeDecl), methodName, 0, new HashSet<>(), contextCu);
}
}
if (narrowed != null && !narrowed.isEmpty()) {
constants.addAll(narrowed);
handledByLombok = true;
}
}
if (!handledByLombok) {
for (Object argObj : cic.arguments()) {
extractConstantsFromArgument((Expression) argObj, constants);
}
}
}
}
Expression currentMi = mi;
while (currentMi instanceof MethodInvocation chainMi) {
String chainName = chainMi.getName().getIdentifier();
if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) {
extractConstantsFromExpression((Expression) chainMi.arguments().get(1), constants);
} else if ((chainName.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) {
extractConstantsFromExpression((Expression) chainMi.arguments().get(0), constants);
}
currentMi = chainMi.getExpression();
}
TypeDeclaration td = findEnclosingType(mi);
if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof ThisExpression)) {
List<String> values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new HashSet<>(), null);
if (values != null) constants.addAll(values);
}
}
}
public void extractConstantsFromArgument(Expression argObj, List<String> constants) {
if (argObj instanceof ClassInstanceCreation innerCic) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
if ("String".equals(typeName)) {
extractConstantsFromExpression(argObj, constants);
}
} else {
extractConstantsFromExpression(argObj, constants);
}
}
public List<String> resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) {
if (className != null && className.contains(".")) {
String prefix = className.substring(0, className.lastIndexOf('.'));
String suffix = className.substring(className.lastIndexOf('.') + 1);
TypeDeclaration prefixTd = contextCu != null ? context.getTypeDeclaration(prefix, contextCu) : context.getTypeDeclaration(prefix);
if (prefixTd == null) prefixTd = context.getTypeDeclaration(prefix);
boolean isKnownType = prefixTd != null || context.getEnumValues(prefix) != null;
if (isKnownType) {
String simplePrefix = prefix.contains(".") ? prefix.substring(prefix.lastIndexOf('.') + 1) : prefix;
return Collections.singletonList(simplePrefix + "." + suffix);
}
}
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
if (td == null) td = context.getTypeDeclaration(className);
if (td == null) return null;
final List<String> resolvedConstants = new ArrayList<>();
for (MethodDeclaration md : td.getMethods()) {
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
public boolean visit(ReturnStatement node) {
if (variableTracer != null) {
List<Expression> tracedReturns = variableTracer.traceVariableAll(node.getExpression());
for (Expression tracedRet : tracedReturns) {
extractConstantsFromExpression(tracedRet, resolvedConstants);
}
}
return super.visit(node);
}
});
}
if (!resolvedConstants.isEmpty()) {
return resolvedConstants;
}
}
return null;
}
public List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
if (depth > 20) return Collections.emptyList();
String fqn = className + "." + methodName;
if (!visited.add(fqn)) return Collections.emptyList();
List<String> constants = new ArrayList<>();
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
if (tempTd == null) {
tempTd = context.getTypeDeclaration(className);
}
final TypeDeclaration td = tempTd;
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
Expression retExpr = node.getExpression();
if (retExpr != null) {
boolean handled = false;
if (retExpr instanceof MethodInvocation mi && calledMethodResolver != null) {
String called = calledMethodResolver.apply(mi);
if (called != null && called.contains(".")) {
if (visited.contains(called)) {
handled = true;
} else {
String cName = called.substring(0, called.lastIndexOf('.'));
String mName = called.substring(called.lastIndexOf('.') + 1);
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
} else if (retExpr instanceof SuperMethodInvocation smi) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
String called = superFqn + "." + smi.getName().getIdentifier();
if (visited.contains(called)) {
handled = true;
} else {
String cName = superFqn;
String mName = smi.getName().getIdentifier();
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
handled = true;
}
}
}
}
if (!handled && variableTracer != null && constructorAnalyzer != null) {
List<Expression> tracedRetExprs = variableTracer.traceVariableAll(retExpr);
for (Expression tracedRetExpr : tracedRetExprs) {
extractConstantsFromExpression(tracedRetExpr, constants);
String val = constantResolver.resolve(tracedRetExpr, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
String parsed = parseEnumSetElement(eVal);
if (!constants.contains(parsed)) constants.add(parsed);
}
} else {
if (!constants.contains(val)) constants.add(val);
}
} else if (tracedRetExpr instanceof SimpleName sn) {
List<String> consts = constructorAnalyzer.traceFieldInConstructors(td, sn.getIdentifier(), context, visited);
if (!consts.isEmpty()) {
constants.addAll(consts);
}
} else if (tracedRetExpr instanceof FieldAccess fa) {
List<String> consts = constructorAnalyzer.traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited);
if (!consts.isEmpty()) {
constants.addAll(consts);
}
}
}
}
}
return super.visit(node);
}
});
} else {
List<String> impls = context.getImplementations(className);
if (impls != null) {
for (String implName : impls) {
List<String> delegationResult = resolveMethodReturnConstant(implName, methodName, depth + 1, visited, contextCu);
constants.addAll(delegationResult);
}
}
}
}
visited.remove(fqn);
return constants;
}
public String parseEnumSetElement(String eVal) {
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
}
private Block findEnclosingBlock(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof Block)) {
parent = parent.getParent();
}
return (Block) parent;
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
}

View File

@@ -0,0 +1,718 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class ConstructorAnalyzer {
private final CodebaseContext context;
private final ConstantResolver constantResolver;
private VariableTracer variableTracer;
private ConstantExtractor constantExtractor;
@FunctionalInterface
public interface RhsResolver {
String resolve(Expression rhs, Map<String, String> paramValues, TypeDeclaration td);
}
public ConstructorAnalyzer(CodebaseContext context, ConstantResolver constantResolver) {
this.context = context;
this.constantResolver = constantResolver;
}
public void setVariableTracer(VariableTracer variableTracer) {
this.variableTracer = variableTracer;
}
public void setConstantExtractor(ConstantExtractor constantExtractor) {
this.constantExtractor = constantExtractor;
}
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
List<String> results = new ArrayList<>();
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
Expression right = frag.getInitializer();
if (variableTracer != null && constantExtractor != null) {
List<Expression> tracedRights = variableTracer.traceVariableAll(right);
for (Expression tracedRight : tracedRights) {
if (tracedRight instanceof MethodInvocation mi) {
String calledName = mi.getName().getIdentifier();
String targetClass = context.getFqn(td);
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
if (mi.getExpression() == null) {
TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu);
if (targetTd != null) {
targetClass = context.getFqn(targetTd);
}
}
results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
} else {
String val = constantResolver.resolve(tracedRight, context);
if (val != null) results.add(val);
}
}
}
}
}
}
ASTVisitor assignmentVisitor = new ASTVisitor() {
@Override
public boolean visit(Assignment node) {
Expression left = node.getLeftHandSide();
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
Expression right = node.getRightHandSide();
if (variableTracer != null && constantExtractor != null) {
List<Expression> tracedRights = variableTracer.traceVariableAll(right);
for (Expression tracedRight : tracedRights) {
if (tracedRight instanceof MethodInvocation mi) {
String calledName = mi.getName().getIdentifier();
String targetClass = context.getFqn(td);
if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) {
targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName();
} else if (mi.getExpression() instanceof SimpleName) {
String exprName = ((SimpleName) mi.getExpression()).getIdentifier();
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu);
if (targetTd != null) {
targetClass = context.getFqn(targetTd);
}
} else if (mi.getExpression() == null) {
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu);
if (targetTd != null) {
targetClass = context.getFqn(targetTd);
}
}
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu));
} else {
String val = constantResolver.resolve(tracedRight, context);
if (val != null) results.add(val);
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(ConstructorInvocation node) {
processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited);
return super.visit(node);
}
@Override
public boolean visit(SuperConstructorInvocation node) {
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
String superFqn = context.getSuperclassFqn(enclosingTd);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited);
}
}
return super.visit(node);
}
};
for (MethodDeclaration md : td.getMethods()) {
if (md.isConstructor() && md.getBody() != null) {
md.getBody().accept(assignmentVisitor);
}
}
for (Object bodyDecl : td.bodyDeclarations()) {
if (bodyDecl instanceof Initializer init && init.getBody() != null) {
init.getBody().accept(assignmentVisitor);
}
}
return results;
}
public Map<String, String> buildFieldValuesFromCIC(
TypeDeclaration td,
ClassInstanceCreation cic,
CodebaseContext context,
RhsResolver rhsResolver,
Map<String, String> initialLocals) {
String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName);
if (actualTd == null) {
actualTd = td;
}
Map<String, String> fieldValues = new HashMap<>();
Set<String> visitedCtors = new HashSet<>();
buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors, rhsResolver, initialLocals);
return fieldValues;
}
public Map<String, String> buildFieldValuesFromCIC(
TypeDeclaration td,
ClassInstanceCreation cic,
CodebaseContext context,
RhsResolver rhsResolver) {
return buildFieldValuesFromCIC(td, cic, context, rhsResolver, null);
}
public Map<String, String> buildFieldValuesFromCIC(
TypeDeclaration td,
ClassInstanceCreation cic,
CodebaseContext context) {
return buildFieldValuesFromCIC(td, cic, context, null, null);
}
private void buildFieldValuesFromCICRecursive(
TypeDeclaration td,
ClassInstanceCreation cic,
CodebaseContext context,
Map<String, String> fieldValues,
Set<String> visitedCtors,
RhsResolver rhsResolver,
Map<String, String> initialLocals) {
String tdKey = context.getFqn(td);
if (!visitedCtors.add(tdKey)) return;
IMethodBinding resolvedCtor = cic.resolveConstructorBinding();
for (MethodDeclaration ctor : td.getMethods()) {
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
boolean matches = false;
if (resolvedCtor != null && ctor.resolveBinding() != null) {
matches = resolvedCtor.isEqualTo(ctor.resolveBinding()) || resolvedCtor.getKey().equals(ctor.resolveBinding().getKey());
} else {
matches = ctor.parameters().size() == cic.arguments().size();
}
if (!matches) continue;
Map<String, String> paramValues = new HashMap<>();
for (int i = 0; i < ctor.parameters().size(); i++) {
String pName = ((SingleVariableDeclaration) ctor.parameters().get(i)).getName().getIdentifier();
Expression argExpr = (Expression) cic.arguments().get(i);
List<String> consts = new ArrayList<>();
if (constantExtractor != null) {
constantExtractor.extractConstantsFromArgument(argExpr, consts);
}
if (!consts.isEmpty()) {
paramValues.put(pName, consts.get(0));
} else {
String resolved = null;
if (argExpr instanceof SimpleName sn && initialLocals != null && initialLocals.containsKey(sn.getIdentifier())) {
resolved = initialLocals.get(sn.getIdentifier());
}
if (resolved == null) {
resolved = constantResolver.resolve(argExpr, context);
}
if (resolved != null) paramValues.put(pName, resolved);
}
}
if (paramValues.isEmpty()) continue;
ctor.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment node) {
Expression lhs = node.getLeftHandSide();
Expression rhs = node.getRightHandSide();
String fieldName = null;
if (lhs instanceof FieldAccess fa
&& fa.getExpression() instanceof ThisExpression) {
fieldName = fa.getName().getIdentifier();
} else if (lhs instanceof SimpleName sn
&& !paramValues.containsKey(sn.getIdentifier())) {
fieldName = sn.getIdentifier();
}
if (fieldName != null) {
String paramVal = null;
if (rhs instanceof SimpleName snRhs) {
paramVal = paramValues.get(snRhs.getIdentifier());
} else if (rhsResolver != null) {
paramVal = rhsResolver.resolve(rhs, paramValues, td);
}
if (paramVal != null) {
fieldValues.put(fieldName, paramVal);
fieldValues.put("this." + fieldName, paramVal);
}
}
return super.visit(node);
}
});
ctor.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(SuperConstructorInvocation sci) {
IMethodBinding binding = sci.resolveConstructorBinding();
if (binding != null) {
ITypeBinding declaringClass = binding.getDeclaringClass();
if (declaringClass != null) {
TypeDeclaration superTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
if (superTd != null) {
processSuperConstructorArgs(superTd, binding, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
return super.visit(sci);
}
}
}
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
processSuperConstructorArgs(superTd, null, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
}
}
return super.visit(sci);
}
@Override
public boolean visit(ConstructorInvocation ci) {
IMethodBinding binding = ci.resolveConstructorBinding();
processSuperConstructorArgs(td, binding, ci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
return super.visit(ci);
}
});
break;
}
}
private void processSuperConstructorArgs(
TypeDeclaration superTd,
IMethodBinding resolvedBinding,
List<?> superArgs,
Map<String, String> subClassParamValues,
CodebaseContext context,
Map<String, String> fieldValues,
Set<String> visitedCtors,
RhsResolver rhsResolver) {
if (superTd == null) return;
for (MethodDeclaration superCtor : superTd.getMethods()) {
if (!superCtor.isConstructor() || superCtor.getBody() == null) continue;
boolean matches = false;
if (resolvedBinding != null && superCtor.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(superCtor.resolveBinding()) || resolvedBinding.getKey().equals(superCtor.resolveBinding().getKey());
} else {
matches = superCtor.parameters().size() == superArgs.size();
}
if (!matches) continue;
Map<String, String> superParamValues = new HashMap<>();
for (int i = 0; i < superCtor.parameters().size(); i++) {
String pName = ((SingleVariableDeclaration) superCtor.parameters().get(i)).getName().getIdentifier();
Expression argExpr = (Expression) superArgs.get(i);
String resolved = null;
if (argExpr instanceof SimpleName sn) {
resolved = subClassParamValues.get(sn.getIdentifier());
}
if (resolved == null) {
List<String> consts = new ArrayList<>();
if (constantExtractor != null) {
constantExtractor.extractConstantsFromArgument(argExpr, consts);
}
if (!consts.isEmpty()) {
resolved = consts.get(0);
} else {
resolved = constantResolver.resolve(argExpr, context);
}
}
if (resolved != null) {
superParamValues.put(pName, resolved);
}
}
if (superParamValues.isEmpty()) continue;
superCtor.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment node) {
Expression lhs = node.getLeftHandSide();
Expression rhs = node.getRightHandSide();
String fieldName = null;
if (lhs instanceof FieldAccess fa
&& fa.getExpression() instanceof ThisExpression) {
fieldName = fa.getName().getIdentifier();
} else if (lhs instanceof SimpleName sn
&& !superParamValues.containsKey(sn.getIdentifier())) {
fieldName = sn.getIdentifier();
}
if (fieldName != null) {
String paramVal = null;
if (rhs instanceof SimpleName snRhs) {
paramVal = superParamValues.get(snRhs.getIdentifier());
} else if (rhsResolver != null) {
paramVal = rhsResolver.resolve(rhs, superParamValues, superTd);
}
if (paramVal != null) {
fieldValues.put(fieldName, paramVal);
fieldValues.put("this." + fieldName, paramVal);
}
}
return super.visit(node);
}
});
superCtor.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(SuperConstructorInvocation sci) {
IMethodBinding binding = sci.resolveConstructorBinding();
if (binding != null) {
ITypeBinding declaringClass = binding.getDeclaringClass();
if (declaringClass != null) {
TypeDeclaration nextSuperTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
if (nextSuperTd != null) {
processSuperConstructorArgs(nextSuperTd, binding, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
return super.visit(sci);
}
}
}
String superFqn = context.getSuperclassFqn(superTd);
if (superFqn != null) {
TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
if (nextSuperTd != null) {
processSuperConstructorArgs(nextSuperTd, null, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
}
}
return super.visit(sci);
}
@Override
public boolean visit(ConstructorInvocation ci) {
IMethodBinding binding = ci.resolveConstructorBinding();
processSuperConstructorArgs(superTd, binding, ci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
return super.visit(ci);
}
});
break;
}
}
public String evaluateMethodCallInConstructor(
Expression rhs,
Map<String, String> paramValues,
TypeDeclaration td) {
if (!(rhs instanceof MethodInvocation mi)) {
return null;
}
String methodName = mi.getName().getIdentifier();
Expression receiver = mi.getExpression();
if (receiver != null && !(receiver instanceof ThisExpression)) {
return null;
}
MethodDeclaration method = context.findMethodDeclaration(td, methodName, true);
if (method == null || method.getBody() == null) {
return null;
}
Map<String, String> localVars = new HashMap<>(paramValues);
return constantResolver.evaluateMethodBodyWithLocals(method, localVars, context, new HashSet<>());
}
public ClassInstanceCreation findReturnedCIC(
String className, String methodName, CodebaseContext context) {
return findReturnedCIC(className, methodName, context, new HashSet<>(), 0);
}
private ClassInstanceCreation findReturnedCIC(
String className, String methodName, CodebaseContext context,
Set<String> visited, int depth) {
if (depth > 50 || !visited.add(className + "#" + methodName)) {
return null;
}
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
ClassInstanceCreation[] result = {null};
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement rs) {
if (rs.getExpression() instanceof ClassInstanceCreation cic) {
result[0] = cic;
}
return false;
}
});
if (result[0] != null) return result[0];
}
}
List<String> impls = context.getImplementations(className);
for (String impl : impls) {
ClassInstanceCreation cic = findReturnedCIC(impl, methodName, context, visited, depth + 1);
if (cic != null) return cic;
}
return null;
}
public List<String> resolveInlineInstantiationGetterResult(
ClassInstanceCreation cic,
String getterName,
CodebaseContext context,
Set<String> visited) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
CompilationUnit contextCu = cic.getRoot() instanceof CompilationUnit ? (CompilationUnit) cic.getRoot() : null;
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName);
if (td == null) td = context.getTypeDeclaration(typeName);
if (td == null) return Collections.emptyList();
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
if (getter == null || getter.getBody() == null) return Collections.emptyList();
String getterKey = typeName + "#" + getterName;
if (!visited.add(getterKey)) {
return Collections.emptyList();
}
try {
Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context);
if (fieldValues.isEmpty()) return Collections.emptyList();
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
return result != null ? List.of(result) : Collections.emptyList();
} finally {
visited.remove(getterKey);
}
}
public int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) {
boolean isAllArgsConstructor = false;
boolean isRequiredArgsConstructor = false;
boolean isData = false;
boolean isValue = false;
for (Object modObj : td.modifiers()) {
if (modObj instanceof MarkerAnnotation ma) {
String name = ma.getTypeName().getFullyQualifiedName();
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
} else if (modObj instanceof NormalAnnotation na) {
String name = na.getTypeName().getFullyQualifiedName();
if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true;
if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true;
if (name.equals("Data") || name.equals("lombok.Data")) isData = true;
if (name.equals("Value") || name.equals("lombok.Value")) isValue = true;
}
}
if (!isAllArgsConstructor && !isRequiredArgsConstructor && !isData && !isValue) {
return -1;
}
int index = 0;
for (FieldDeclaration fd : td.getFields()) {
boolean isStatic = false;
boolean isFinal = false;
boolean hasNonNull = false;
for (Object modObj : fd.modifiers()) {
if (modObj instanceof Modifier mod) {
if (mod.isStatic()) isStatic = true;
if (mod.isFinal()) isFinal = true;
} else if (modObj instanceof MarkerAnnotation ma) {
if (ma.getTypeName().getFullyQualifiedName().endsWith("NonNull")) hasNonNull = true;
}
}
if (isStatic) continue;
if (isRequiredArgsConstructor && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
if (isData && !isAllArgsConstructor && !isFinal && !hasNonNull) continue;
for (Object fragObj : fd.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(fieldName)) {
return index;
}
index++;
}
}
return -1;
}
private void processConstructorInvocationArgs(List<?> arguments, TypeDeclaration targetTd, ASTNode callNode, List<String> results, String fieldName, CodebaseContext context, Set<String> visited) {
if (targetTd == null) return;
MethodDeclaration callerMd = findEnclosingMethod(callNode);
IMethodBinding resolvedBinding = null;
if (callNode instanceof ConstructorInvocation ci) {
resolvedBinding = ci.resolveConstructorBinding();
} else if (callNode instanceof SuperConstructorInvocation sci) {
resolvedBinding = sci.resolveConstructorBinding();
}
for (MethodDeclaration otherMd : targetTd.getMethods()) {
boolean matches = false;
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
} else {
matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd;
}
if (matches) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new HashSet<>());
if (targetIdx >= 0 && targetIdx < arguments.size()) {
Expression arg = (Expression) arguments.get(targetIdx);
String val = constantResolver.resolve(arg, context);
if (val != null) {
if (val.startsWith("ENUM_SET:")) {
for (String eVal : val.substring(9).split(",")) {
if (constantExtractor != null) {
results.add(constantExtractor.parseEnumSetElement(eVal));
}
}
} else {
results.add(val);
}
}
}
}
}
}
private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, Set<MethodDeclaration> visited) {
if (constructorMd == null || constructorMd.getBody() == null) return -1;
if (!visited.add(constructorMd)) return -1;
final int[] foundIdx = {-1};
constructorMd.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment asn) {
Expression left = asn.getLeftHandSide();
if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) ||
(left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) ||
(left instanceof SuperFieldAccess sfa && sfa.getName().getIdentifier().equals(fieldName))) {
Expression right = asn.getRightHandSide();
String rightName = extractVariableName(right);
if (rightName != null) {
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(rightName)) {
foundIdx[0] = i;
}
}
}
}
return super.visit(asn);
}
@Override
public boolean visit(ConstructorInvocation node) {
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
for (MethodDeclaration otherMd : enclosingTd.getMethods()) {
boolean matches = false;
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
} else {
matches = otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size();
}
if (matches) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
String argName = extractVariableName(arg);
if (argName != null) {
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperConstructorInvocation node) {
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
TypeDeclaration enclosingTd = findEnclosingType(node);
if (enclosingTd != null) {
String superFqn = context.getSuperclassFqn(enclosingTd);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
for (MethodDeclaration otherMd : superTd.getMethods()) {
boolean matches = false;
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
} else {
matches = otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size();
}
if (matches) {
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
Expression arg = (Expression) node.arguments().get(targetIdx);
String argName = extractVariableName(arg);
if (argName != null) {
for (int i = 0; i < constructorMd.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
if (svd.getName().getIdentifier().equals(argName)) {
foundIdx[0] = i;
}
}
}
}
}
}
}
}
}
return super.visit(node);
}
});
return foundIdx[0];
}
private String extractVariableName(Expression expr) {
if (expr instanceof SimpleName sn) return sn.getIdentifier();
if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier();
if (expr instanceof SuperFieldAccess sfa) return sfa.getName().getIdentifier();
if (expr instanceof CastExpression ce) return extractVariableName(ce.getExpression());
if (expr instanceof ParenthesizedExpression pe) return extractVariableName(pe.getExpression());
if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) {
if (!mi.arguments().isEmpty()) return extractVariableName((Expression)mi.arguments().get(0));
}
return null;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
return (MethodDeclaration) parent;
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
}

View File

@@ -0,0 +1,162 @@
package click.kamil.springstatemachineexporter.analysis.service;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Slf4j
public class DynamicClasspathResolver {
public List<String> resolveClasspath(Path projectRoot) {
log.info("Attempting dynamic classpath resolution for project at {}", projectRoot);
if (projectRoot == null || !Files.exists(projectRoot)) {
return Collections.emptyList();
}
if (Files.exists(projectRoot.resolve("pom.xml"))) {
return resolveMaven(projectRoot);
} else if (Files.exists(projectRoot.resolve("build.gradle")) || Files.exists(projectRoot.resolve("build.gradle.kts"))) {
return resolveGradle(projectRoot);
}
log.info("No Maven or Gradle configuration found at {}. Proceeding without dynamic classpath.", projectRoot);
return Collections.emptyList();
}
protected List<String> resolveMaven(Path projectRoot) {
log.info("Maven project detected. Resolving dependencies...");
Path cpFile = null;
try {
cpFile = Files.createTempFile("state_machine_exporter_cp", ".txt");
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
if (mvnCmd == null) {
log.warn("Maven executable not found.");
return Collections.emptyList();
}
ProcessBuilder pb = new ProcessBuilder(
mvnCmd,
"-q",
"dependency:build-classpath",
"-DincludeScope=compile",
"-Dmdep.outputFile=" + cpFile.toAbsolutePath().toString()
);
pb.directory(projectRoot.toFile());
runProcess(pb);
if (Files.exists(cpFile)) {
String cpString = Files.readString(cpFile).trim();
if (!cpString.isEmpty()) {
return Arrays.asList(cpString.split(File.pathSeparator));
}
}
} catch (Exception e) {
log.error("Failed to dynamically resolve Maven classpath", e);
} finally {
if (cpFile != null) {
try {
Files.deleteIfExists(cpFile);
} catch (IOException ignored) {}
}
}
return Collections.emptyList();
}
protected List<String> resolveGradle(Path projectRoot) {
log.info("Gradle project detected. Resolving dependencies...");
Path initScript = null;
try {
initScript = Files.createTempFile("state_machine_exporter_init", ".gradle");
String gradleCmd = getCommand(projectRoot, "gradlew", "gradle");
if (gradleCmd == null) {
log.warn("Gradle executable not found.");
return Collections.emptyList();
}
String scriptContent = """
allprojects {
task smePrintClasspath {
doLast {
def cp = []
if (configurations.findByName("compileClasspath")) {
cp = configurations.compileClasspath.files
}
println "SME_CLASSPATH_MARKER:" + cp.join(File.pathSeparator)
}
}
}
""";
Files.writeString(initScript, scriptContent);
ProcessBuilder pb = new ProcessBuilder(
gradleCmd,
"-q",
"-I", initScript.toAbsolutePath().toString(),
"smePrintClasspath"
);
pb.directory(projectRoot.toFile());
String output = runProcessAndGetOutput(pb);
for (String line : output.split("\\r?\\n")) {
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
if (!cpString.isEmpty()) {
return Arrays.asList(cpString.split(File.pathSeparator));
}
}
}
} catch (Exception e) {
log.error("Failed to dynamically resolve Gradle classpath", e);
} finally {
if (initScript != null) {
try {
Files.deleteIfExists(initScript);
} catch (IOException ignored) {}
}
}
return Collections.emptyList();
}
private String getCommand(Path projectRoot, String wrapper, String systemBinary) {
Path wrapperUnix = projectRoot.resolve(wrapper);
Path wrapperWin = projectRoot.resolve(wrapper + ".bat");
boolean isWin = System.getProperty("os.name").toLowerCase().contains("win");
if (isWin && Files.exists(wrapperWin)) return wrapperWin.toAbsolutePath().toString();
if (!isWin && Files.exists(wrapperUnix)) {
// Need absolute path like ./gradlew
return wrapperUnix.toAbsolutePath().toString();
}
return systemBinary;
}
protected void runProcess(ProcessBuilder pb) throws IOException, InterruptedException {
Process p = pb.start();
p.waitFor();
}
protected String runProcessAndGetOutput(ProcessBuilder pb) throws IOException, InterruptedException {
Process p = pb.start();
StringBuilder out = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
out.append(line).append("\n");
}
}
p.waitFor();
return out.toString();
}
}

View File

@@ -172,44 +172,119 @@ public class GenericEventDetector {
}
private String extractSourceState(ASTNode node) {
ASTNode current = node.getParent();
ASTNode current = node;
while (current != null && !(current instanceof MethodDeclaration)) {
if (current instanceof IfStatement ifStmt) {
Expression expr = ifStmt.getExpression();
String state = extractStateFromExpression(expr);
if (state != null) return state;
} else if (current instanceof SwitchCase switchCase) {
if (!switchCase.expressions().isEmpty()) {
return getSimpleNameString((Expression) switchCase.expressions().get(0));
ASTNode parent = current.getParent();
// 1. Direct parent is IfStatement wrapper (e.g., if (state == X) { sm.sendEvent() })
if (parent instanceof IfStatement ifStmt) {
// Only consider if our current node is in the 'then' or 'else' part, not the condition
if (ifStmt.getExpression() != current) {
Expression expr = ifStmt.getExpression();
String state = extractStateFromExpression(expr);
if (state != null) return state;
}
} else if (current instanceof SwitchStatement switchStmt) {
// If it's a switch block but we haven't hit a SwitchCase directly (AST hierarchy usually has SwitchCase as siblings of block statements)
// Actually, walking up from the node, we will hit the SwitchStatement, but we need the SwitchCase right before us in the block.
ASTNode parentBlock = current;
// It's complex to walk siblings. A simpler heuristic is to just use IfStatement and SwitchCase (if wrapped correctly).
}
current = current.getParent();
// 2. Parent is a Block or SwitchStatement: perform sibling traversal
if (parent instanceof Block block) {
String state = extractStateFromSiblings(current, block.statements());
if (state != null) return state;
} else if (parent instanceof SwitchStatement switchStmt) {
String state = extractStateFromSiblings(current, switchStmt.statements());
if (state != null) return state;
}
current = parent;
}
return null;
}
private String extractStateFromSiblings(ASTNode currentNode, List<?> statements) {
int index = statements.indexOf(currentNode);
if (index <= 0) return null; // No previous siblings
// Walk backwards through siblings
for (int i = index - 1; i >= 0; i--) {
Object stmtObj = statements.get(i);
if (stmtObj instanceof SwitchCase switchCase) {
if (!switchCase.expressions().isEmpty()) {
return getSimpleNameString((Expression) switchCase.expressions().get(0));
}
} else if (stmtObj instanceof IfStatement ifStmt) {
// Guard clause detection: check if the 'then' block has a return/throw
if (isGuardClause(ifStmt.getThenStatement())) {
Expression expr = ifStmt.getExpression();
// If it's a guard clause, it usually checks `state != EXPECTED`, so the true state *is* EXPECTED
String state = extractStateFromExpression(expr);
if (state != null) return state;
}
}
}
return null;
}
private boolean isGuardClause(Statement thenStatement) {
if (thenStatement instanceof ReturnStatement || thenStatement instanceof ThrowStatement) {
return true;
}
if (thenStatement instanceof Block block) {
for (Object obj : block.statements()) {
if (obj instanceof ReturnStatement || obj instanceof ThrowStatement) {
return true;
}
}
}
return false;
}
private String extractStateFromExpression(Expression expr) {
if (expr instanceof InfixExpression infix) {
if (infix.getOperator() == InfixExpression.Operator.EQUALS) {
if (infix.getOperator() == InfixExpression.Operator.EQUALS || infix.getOperator() == InfixExpression.Operator.NOT_EQUALS) {
Expression left = infix.getLeftOperand();
Expression right = infix.getRightOperand();
// Usually one is a method call like getState(), the other is an enum
if (left instanceof QualifiedName || left instanceof SimpleName && !(right instanceof SimpleName)) {
// Ignore null checks entirely
if (left instanceof NullLiteral || right instanceof NullLiteral) {
return null;
}
// Usually one is a method call like getState() or a variable like `state`
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
// If one is a QualifiedName (enum constant) or StringLiteral, it's likely the state
if (left instanceof QualifiedName || left instanceof StringLiteral || left instanceof FieldAccess) {
return getSimpleNameString(left);
}
if (right instanceof QualifiedName || right instanceof SimpleName && !(left instanceof SimpleName)) {
if (right instanceof QualifiedName || right instanceof StringLiteral || right instanceof FieldAccess) {
return getSimpleNameString(right);
}
return getSimpleNameString(right); // Fallback
// Fallback
return getSimpleNameString(right);
}
} else if (expr instanceof MethodInvocation mi) {
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
return getSimpleNameString((Expression) mi.arguments().get(0));
String methodName = mi.getName().getIdentifier();
if (("equals".equals(methodName) || "equalsIgnoreCase".equals(methodName)) && !mi.arguments().isEmpty()) {
Expression receiver = mi.getExpression();
Expression arg = (Expression) mi.arguments().get(0);
// If receiver is null (e.g., implicit this), fall back to arg
if (receiver == null) {
return getSimpleNameString(arg);
}
// Prioritize the one that looks like a constant (QualifiedName or StringLiteral)
if (receiver instanceof QualifiedName || receiver instanceof StringLiteral || receiver instanceof FieldAccess) {
return getSimpleNameString(receiver);
}
if (arg instanceof QualifiedName || arg instanceof StringLiteral || arg instanceof FieldAccess) {
return getSimpleNameString(arg);
}
// Fallback to receiver
return getSimpleNameString(receiver);
}
}
return null;
@@ -268,6 +343,10 @@ public class GenericEventDetector {
}
}
if (expr instanceof ClassInstanceCreation cic) {
return cic.getType().toString();
}
if (!(expr instanceof MethodInvocation mi)) return null;
// Trace back chain: MessageBuilder.withPayload(event).setHeader(...).build()

View File

@@ -0,0 +1,575 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
@Slf4j
public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
protected String currentMethodFqn;
protected Map<String, List<CallEdge>> graph;
public HeuristicCallGraphEngine(CodebaseContext context) {
super(context);
}
protected Map<String, List<CallEdge>> buildCallGraph() {
graph = new HashMap<>();
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration td = findEnclosingType(md);
if (td != null) {
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
List<String> args = resolveArguments(node.arguments());
for (String calledMethod : calledMethods) {
String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
}
for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) {
String typeName = emr.getExpression().toString();
if ("this".equals(typeName) || "super".equals(typeName)) {
TypeDeclaration td2 = findEnclosingType(node);
if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) {
List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs, receiver));
}
}
} else {
String fallbackTypeFqn = null;
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
if (binding != null) {
fallbackTypeFqn = binding.getQualifiedName();
} else if (emr.getExpression() instanceof SimpleName sn) {
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
}
if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
String receiver = getReceiverString(node.getExpression());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs, receiver));
List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver));
}
}
}
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperMethodInvocation node) {
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration tdOuter = findEnclosingType(md);
if (tdOuter != null) {
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
String methodName = node.getName().getIdentifier();
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
String calledMethod = null;
if (superTd != null) {
calledMethod = resolveMethodInType(superTd, methodName);
}
if (calledMethod == null) {
calledMethod = superFqn + "." + methodName;
}
List<String> args = resolveArguments(node.arguments());
String receiver = "super";
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
}
}
}
}
return super.visit(node);
}
});
}
return graph;
}
@Override
protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
// Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
// where we can substitute the actual receiver from the call edge
boolean isThisOrSuperGetter = (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi
&& (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression || mi.getExpression() instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation)
&& mi.arguments().isEmpty()) || (originalExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi && smi.arguments().isEmpty());
if (isThisOrSuperGetter) {
return originalExpr.toString(); // Preserve "this.getTransitionType()" or "super.getEvent()" for later substitution
}
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized
// from a ClassInstanceCreation (directly or via a factory method). This avoids
// the broad ENUM_SET fallback when the getter transforms one enum to another.
if (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation getterMi
&& getterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName instanceSn
&& getterMi.arguments().isEmpty()) {
String narrowed = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
if (narrowed != null) {
return narrowed;
}
}
return resolvedValue;
}
protected List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
String baseCalled = resolveCalledMethod(node);
if (baseCalled == null) return Collections.emptyList();
List<String> allResolved = new ArrayList<>();
allResolved.add(baseCalled);
if (baseCalled.contains(".")) {
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
String definingClass = findDefiningClass(className, methodName);
if (definingClass != null && !definingClass.equals(className)) {
allResolved.set(0, definingClass + "." + methodName);
className = definingClass;
}
List<String> impls = context.getImplementations(className);
for (String impl : impls) {
allResolved.add(impl + "." + methodName);
}
}
return allResolved;
}
private String findDefiningClass(String className, String methodName) {
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) return null;
String resolvedMethodFqn = resolveMethodInTypeHierarchy(td, methodName);
if (resolvedMethodFqn != null) {
return resolvedMethodFqn.substring(0, resolvedMethodFqn.lastIndexOf('.'));
}
return null;
}
protected String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier();
if (receiver == null) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return resolveMethodInTypeHierarchy(td, methodName);
}
return null;
}
ITypeBinding binding = receiver.resolveTypeBinding();
if (binding != null) {
return binding.getQualifiedName() + "." + methodName;
}
if (receiver instanceof ThisExpression) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return context.getFqn(td) + "." + methodName;
}
}
if (receiver instanceof SimpleName sn) {
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
if (fallbackTypeFqn != null) {
return fallbackTypeFqn + "." + methodName;
}
String receiverName = sn.getIdentifier();
return receiverName + "." + methodName;
}
if (receiver instanceof QualifiedName qn) {
String fqn = qn.getFullyQualifiedName();
TypeDeclaration td = context.getTypeDeclaration(fqn);
if (td != null) {
return fqn + "." + methodName;
}
return fqn + "." + methodName;
}
return null;
}
protected String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
String varName = receiverNameNode.getIdentifier();
CompilationUnit cu = receiverNameNode.getRoot() instanceof CompilationUnit ? (CompilationUnit) receiverNameNode.getRoot() : null;
TypeDeclaration staticTd = context.getTypeDeclaration(varName, cu);
if (staticTd != null) {
return context.getFqn(staticTd);
}
TypeDeclaration fallbackStaticTd = context.getTypeDeclaration(varName);
if (fallbackStaticTd != null) {
return context.getFqn(fallbackStaticTd);
}
// 1. Check local variables in enclosing method
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
if (enclosingMethod != null) {
// Check parameters
for (Object paramObj : enclosingMethod.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
if (svd.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(svd.getType(), receiverNameNode);
}
}
// Check method body (local variables)
if (enclosingMethod.getBody() != null) {
Type[] foundType = new Type[1];
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType();
}
}
return super.visit(node);
}
});
if (foundType[0] != null) {
return resolveTypeToFqn(foundType[0], receiverNameNode);
}
}
}
// 2. Check fields in enclosing class
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
if (enclosingType != null) {
for (FieldDeclaration field : enclosingType.getFields()) {
for (Object fragObj : field.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(field.getType(), receiverNameNode);
}
}
}
}
return null;
}
protected String resolveTypeToFqn(Type type, ASTNode contextNode) {
if (type == null) return null;
String simpleName;
if (type.isSimpleType()) {
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
} else {
simpleName = type.toString();
}
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
return simpleName;
}
/**
* Attempts to narrow the resolution of {@code instanceVar.getterMethod()} when the
* instance variable is initialised from a {@link org.eclipse.jdt.core.dom.ClassInstanceCreation}
* (either directly or via a factory/mapper method).
*
* <p>This handles both:
* <ul>
* <li>Simple pass-through: {@code Wrapper(MyEnum e){this.e=e;} getE(){return e;}}</li>
* <li>Transformation: {@code getSmEvent(){return switch(dtoEnum){case A->SmEvents.X;…};}}</li>
* </ul>
*
* @return the resolved constant string, or {@code null} when narrowing is not possible
*/
private String tryNarrowGetterViaLocalVarChain(
MethodInvocation getterCall, SimpleName instanceSn) {
MethodDeclaration enclosing = findEnclosingMethod(getterCall);
if (enclosing == null || enclosing.getBody() == null) return null;
String varName = instanceSn.getIdentifier();
String getterName = getterCall.getName().getIdentifier();
// Find the declared type and initialiser of the local variable
String[] varTypeName = {null};
Expression[] initExpr = {null};
enclosing.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement vds) {
for (Object fragObj : vds.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
varTypeName[0] = vds.getType().toString();
initExpr[0] = frag.getInitializer();
}
}
return super.visit(vds);
}
});
if (varTypeName[0] == null) return null;
// Obtain the ClassInstanceCreation that created the object
org.eclipse.jdt.core.dom.ClassInstanceCreation cic = null;
String receiverType = null;
if (initExpr[0] instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation directCic) {
cic = directCic;
} else if (initExpr[0] instanceof MethodInvocation factoryMi
&& (factoryMi.getExpression() instanceof SimpleName || factoryMi.getExpression() instanceof QualifiedName)) {
// e.g. "mapper.toMsg(dto)" or "com.example.Factory.toMsg()"
if (factoryMi.getExpression() instanceof SimpleName receiverSn) {
receiverType = resolveReceiverTypeFallback(receiverSn);
} else if (factoryMi.getExpression() instanceof QualifiedName qn) {
receiverType = qn.getFullyQualifiedName();
}
if (receiverType != null) {
cic = findReturnedCIC(receiverType, factoryMi.getName().getIdentifier(), context);
}
}
if (cic == null) {
// Support builder pattern: e.g. EventWrapper.builder().event(TransitionEnum.STATE_X).build()
if (initExpr[0] instanceof MethodInvocation initMi) {
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
Expression currentMi = initMi;
while (currentMi instanceof MethodInvocation chainMi) {
String mName = chainMi.getName().getIdentifier();
if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
if (!chainMi.arguments().isEmpty()) {
return chainMi.arguments().get(0).toString();
}
}
currentMi = chainMi.getExpression();
}
}
return null;
}
// Resolve the variable's declared type to a TypeDeclaration
CompilationUnit cu = (CompilationUnit) getterCall.getRoot();
TypeDeclaration varTypeTd = context.getTypeDeclaration(varTypeName[0], cu);
if (varTypeTd == null) varTypeTd = context.getTypeDeclaration(varTypeName[0]);
if (varTypeTd == null) return null;
// Evaluate the getter body with field values substituted from the CIC's constructor args
java.util.Map<String, String> initialLocals = new java.util.HashMap<>();
if (initExpr[0] instanceof MethodInvocation factoryMi) {
MethodDeclaration md = context.findMethodDeclaration(context.getTypeDeclaration(receiverType != null ? receiverType : varTypeName[0]), factoryMi.getName().getIdentifier(), true);
if (md != null) {
for (int i = 0; i < md.parameters().size() && i < factoryMi.arguments().size(); i++) {
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
Expression arg = (Expression) factoryMi.arguments().get(i);
String resolvedVal = constantResolver.resolve(arg, context);
if (resolvedVal != null) {
initialLocals.put(param.getName().getIdentifier(), resolvedVal);
}
}
}
}
java.util.Map<String, String> fieldValues = constructorAnalyzer.buildFieldValuesFromCIC(varTypeTd, cic, context, this::evaluateMethodCallInConstructor, initialLocals);
if (fieldValues.isEmpty()) return null;
// Track setter calls on the variable between its initialization and the getter call
// to capture field updates like e.setType("NEW_VALUE")
trackSetterCallsBetween(enclosing.getBody(), varName, getterCall, fieldValues, context);
MethodDeclaration getter = context.findMethodDeclaration(varTypeTd, getterName, true);
if (getter == null || getter.getBody() == null) return null;
return constantResolver.evaluateMethodBodyWithLocals(
getter, fieldValues, context, new java.util.HashSet<>());
}
/**
* Scans the method body for setter calls or field assignments to the given variable
* that occur between the variable's initialization and the getter call.
* Updates fieldValues with any field values set via setters.
*/
private void trackSetterCallsBetween(org.eclipse.jdt.core.dom.Block methodBody, String varName,
org.eclipse.jdt.core.dom.MethodInvocation getterCall,
java.util.Map<String, String> fieldValues,
CodebaseContext context) {
java.util.List<org.eclipse.jdt.core.dom.ASTNode> statements = new java.util.ArrayList<>();
for (Object stmtObj : methodBody.statements()) {
statements.add((org.eclipse.jdt.core.dom.ASTNode) stmtObj);
}
int varDeclIndex = -1;
int getterCallIndex = -1;
for (int i = 0; i < statements.size(); i++) {
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
// Find variable declaration
if (varDeclIndex == -1) {
if (stmt instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement vds) {
for (Object fragObj : vds.fragments()) {
if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment frag
&& frag.getName().getIdentifier().equals(varName)) {
varDeclIndex = i;
break;
}
}
}
}
// Find getter call
if (getterCallIndex == -1) {
if (stmt.toString().contains(getterCall.toString())) {
getterCallIndex = i;
break;
}
}
}
if (varDeclIndex >= 0 && getterCallIndex > varDeclIndex) {
// Scan statements between var declaration and getter call
for (int i = varDeclIndex + 1; i < getterCallIndex; i++) {
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
org.eclipse.jdt.core.dom.Expression expr = es.getExpression();
// Check for setter calls: var.setField(value)
if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
&& sn.getIdentifier().equals(varName)) {
String methodName = mi.getName().getIdentifier();
// Check if it's a setter (setXxx or xxx)
if (methodName.startsWith("set") && methodName.length() > 3) {
String fieldName = Character.toLowerCase(methodName.charAt(3))
+ methodName.substring(4);
if (!mi.arguments().isEmpty()) {
org.eclipse.jdt.core.dom.Expression arg = (org.eclipse.jdt.core.dom.Expression) mi.arguments().get(0);
String val = constantResolver.resolve(arg, context);
if (val != null) {
fieldValues.put(fieldName, val);
fieldValues.put("this." + fieldName, val);
}
}
}
}
}
// Check for direct field assignments: var.field = value
else if (expr instanceof org.eclipse.jdt.core.dom.Assignment assignment) {
if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
&& sn.getIdentifier().equals(varName)) {
String fieldName = fa.getName().getIdentifier();
org.eclipse.jdt.core.dom.Expression rhs = assignment.getRightHandSide();
String val = constantResolver.resolve(rhs, context);
if (val != null) {
fieldValues.put(fieldName, val);
fieldValues.put("this." + fieldName, val);
}
}
}
}
}
}
}
}
/**
* Extracts a string representation of the receiver expression from a MethodInvocation.
* Handles ThisExpression, SimpleName, and other expression types.
*/
protected String getReceiverString(Expression receiver) {
if (receiver == null) {
return null;
}
if (receiver instanceof ThisExpression) {
return "this";
}
if (receiver instanceof SuperMethodInvocation) {
return "super";
}
if (receiver instanceof SimpleName sn) {
return sn.getIdentifier();
}
return receiver.toString();
}
/**
* Finds the receiver expression used in the caller method when calling the target method.
* Walks the caller's AST to find a MethodInvocation with the target method name
* and returns the receiver string.
*/
protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
int lastDot = callerFqn.lastIndexOf('.');
if (lastDot < 0) return null;
String callerClass = callerFqn.substring(0, lastDot);
String callerMethod = callerFqn.substring(lastDot + 1);
int targetLastDot = targetMethodFqn.lastIndexOf('.');
if (targetLastDot < 0) return null;
String targetMethodName = targetMethodFqn.substring(targetLastDot + 1);
TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
if (callerTd == null) return null;
MethodDeclaration callerMd = context.findMethodDeclaration(callerTd, callerMethod, false);
if (callerMd == null || callerMd.getBody() == null) return null;
final String[] foundReceiver = {null};
callerMd.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if (foundReceiver[0] != null) return false;
String called = resolveCalledMethod(node);
if (called != null && (called.equals(targetMethodFqn) || called.endsWith("." + targetMethodName))) {
foundReceiver[0] = getReceiverString(node.getExpression());
return false;
}
return super.visit(node);
}
});
return foundReceiver[0];
}
}

View File

@@ -0,0 +1,347 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
@Slf4j
public class JdtCallGraphEngine extends AbstractCallGraphEngine {
private final InjectionPointAnalyzer injectionAnalyzer;
private String currentMethodFqn;
private Map<String, List<CallEdge>> graph;
public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) {
super(context);
this.injectionAnalyzer = injectionAnalyzer;
}
protected Map<String, List<CallEdge>> buildCallGraph() {
graph = new HashMap<>();
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration td = findEnclosingType(md);
if (td != null) {
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
List<String> args = resolveArguments(node.arguments());
for (String calledMethod : calledMethods) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) {
String typeName = emr.getExpression().toString();
if ("this".equals(typeName) || "super".equals(typeName)) {
TypeDeclaration td2 = findEnclosingType(node);
if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) {
List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
}
}
} else {
String fallbackTypeFqn = null;
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
if (binding != null) {
fallbackTypeFqn = binding.getQualifiedName();
} else if (emr.getExpression() instanceof SimpleName sn) {
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
}
if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
List<String> implicitArgs = new ArrayList<>();
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(node)) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
}
}
}
}
}
}
}
return super.visit(node);
}
@Override
public boolean visit(SuperMethodInvocation node) {
MethodDeclaration md = findEnclosingMethod(node);
if (md != null) {
TypeDeclaration tdOuter = findEnclosingType(md);
if (tdOuter != null) {
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
String methodName = node.getName().getIdentifier();
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
String calledMethod = null;
if (superTd != null) {
calledMethod = resolveMethodInType(superTd, methodName);
}
if (calledMethod == null) {
calledMethod = superFqn + "." + methodName;
}
List<String> args = resolveArguments(node.arguments());
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
}
}
}
}
return super.visit(node);
}
});
}
return graph;
}
@Override
protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) {
if (expr == null) {
return "null";
}
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
org.eclipse.jdt.core.dom.Expression unwrappedBuilder = unwrapMessageBuilder(expr);
if (unwrappedBuilder != expr) {
expr = unwrappedBuilder;
}
// Trace variable to resolve local variable references
org.eclipse.jdt.core.dom.Expression tracedExpr = traceVariable(expr);
if (tracedExpr instanceof org.eclipse.jdt.core.dom.QualifiedName
|| tracedExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation
|| tracedExpr instanceof org.eclipse.jdt.core.dom.StringLiteral
|| tracedExpr instanceof org.eclipse.jdt.core.dom.NumberLiteral) {
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
}
// Delegate to base class for lambda, method reference, and constructor unwrapping
return super.resolveArgument(expr);
}
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
String baseCalled = resolveCalledMethod(node);
if (baseCalled == null) return Collections.emptyList();
List<String> allResolved = new ArrayList<>();
allResolved.add(baseCalled);
if (baseCalled.contains(".")) {
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
List<String> impls = context.getImplementations(className);
for (String impl : impls) {
allResolved.add(impl + "." + methodName);
}
}
return allResolved;
}
protected String resolveCalledMethod(MethodInvocation node) {
Expression receiver = node.getExpression();
String methodName = node.getName().getIdentifier();
if (receiver == null) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return resolveMethodInType(td, methodName);
}
return null;
}
if (injectionAnalyzer != null) {
IBinding nameBinding = null;
if (receiver instanceof SimpleName sn) {
nameBinding = sn.resolveBinding();
} else if (receiver instanceof FieldAccess fa) {
nameBinding = fa.resolveFieldBinding();
}
if (nameBinding instanceof IVariableBinding varBinding) {
System.out.println("CALLGRAPH RESOLVING BEAN FOR BINDING: " + varBinding.getName() + " calling " + methodName);
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
if (concreteFqn != null) {
System.out.println(" -> RESOLVED CONCRETE FQN: " + concreteFqn);
return concreteFqn + "." + methodName;
} else {
System.out.println(" -> RESOLVE FAILED, RETURNED NULL");
}
} else {
System.out.println("CALLGRAPH NAME BINDING IS NOT VARIABLE: " + nameBinding + " calling " + methodName);
}
}
ITypeBinding binding = receiver.resolveTypeBinding();
if (binding != null) {
String typeName = null;
try {
if (binding.getErasure() != null) {
typeName = binding.getErasure().getQualifiedName();
if (typeName == null || typeName.isEmpty()) {
typeName = binding.getErasure().getName();
}
}
} catch (Exception e) {
// Ignore JDT internal exceptions
}
if (typeName == null || typeName.isEmpty()) {
typeName = binding.getQualifiedName();
if (typeName == null || typeName.isEmpty()) {
typeName = binding.getName();
}
if (typeName != null && typeName.contains("<")) {
typeName = typeName.replaceAll("<.*>", "");
}
}
if (typeName != null && !typeName.isEmpty()) {
return typeName + "." + methodName;
}
}
if (receiver instanceof ThisExpression) {
TypeDeclaration td = findEnclosingType(node);
if (td != null) {
return context.getFqn(td) + "." + methodName;
}
}
if (receiver instanceof SimpleName sn) {
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
if (fallbackTypeFqn != null) {
return fallbackTypeFqn + "." + methodName;
}
String receiverName = sn.getIdentifier();
return receiverName + "." + methodName;
}
return null;
}
private String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
String varName = receiverNameNode.getIdentifier();
// 1. Check local variables in enclosing method
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
if (enclosingMethod != null) {
// Check parameters
for (Object paramObj : enclosingMethod.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
if (svd.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(svd.getType(), receiverNameNode);
}
}
// Check method body (local variables)
if (enclosingMethod.getBody() != null) {
Type[] foundType = new Type[1];
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
foundType[0] = node.getType();
}
}
return super.visit(node);
}
});
if (foundType[0] != null) {
return resolveTypeToFqn(foundType[0], receiverNameNode);
}
}
}
// 2. Check fields in enclosing class
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
if (enclosingType != null) {
for (FieldDeclaration field : enclosingType.getFields()) {
for (Object fragObj : field.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(varName)) {
return resolveTypeToFqn(field.getType(), receiverNameNode);
}
}
}
}
return null;
}
private String resolveTypeToFqn(Type type, ASTNode contextNode) {
if (type == null) return null;
String simpleName;
if (type.isSimpleType()) {
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
} else if (type.isParameterizedType()) {
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
} else {
simpleName = type.toString();
}
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
if (td != null) {
return context.getFqn(td);
}
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + simpleName)) {
return impName;
}
}
return simpleName;
}
private Expression unwrapMessageBuilder(Expression expr) {
if (expr instanceof MethodInvocation mi) {
MethodInvocation current = mi;
while (current != null) {
String name = current.getName().getIdentifier();
if (("withPayload".equals(name) || "just".equals(name)) && !current.arguments().isEmpty()) {
return (Expression) current.arguments().get(0);
}
Expression receiver = current.getExpression();
if (receiver instanceof MethodInvocation nextMi) {
current = nextMi;
} else {
current = null;
}
}
}
return expr;
}
}

View File

@@ -4,6 +4,10 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.CompilationUnit;
@@ -21,7 +25,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
private final GenericEventDetector eventDetector;
private final SpringMvcDetector mvcDetector;
private final MessagingDetector messagingDetector;
private final CallGraphBuilder callGraphBuilder;
private final CallGraphEngine callGraphEngine;
private final LifecycleDetector lifecycleDetector;
private final InterceptorDetector interceptorDetector;
private final SpringComponentDetector componentDetector;
@@ -32,7 +36,17 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
this.messagingDetector = new MessagingDetector(context);
this.callGraphBuilder = new CallGraphBuilder(context);
SpringBeanRegistry registry = new SpringBeanRegistry();
SpringContextScanner scanner = new SpringContextScanner(registry);
for (CompilationUnit cu : context.getCompilationUnits()) {
cu.accept(scanner);
}
SpringDependencyResolver dependencyResolver = new SpringDependencyResolver(registry);
InjectionPointAnalyzer injectionAnalyzer = new InjectionPointAnalyzer(dependencyResolver);
this.callGraphEngine = new JdtCallGraphEngine(context, injectionAnalyzer);
this.lifecycleDetector = new LifecycleDetector(context);
this.interceptorDetector = new InterceptorDetector(context);
this.componentDetector = new SpringComponentDetector(context);
@@ -69,7 +83,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
@Override
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
return callGraphBuilder.findChains(entryPoints, triggers);
return callGraphEngine.findChains(entryPoints, triggers);
}
@Override

View File

@@ -0,0 +1,133 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class TypeResolver {
private final CodebaseContext context;
public TypeResolver(CodebaseContext context) {
this.context = context;
}
public int getParameterIndex(String methodFqn, String paramName) {
if (methodFqn == null || !methodFqn.contains(".")) return -1;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null) {
for (int i = 0; i < md.parameters().size(); i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
if (svd.getName().getIdentifier().equals(paramName)) {
return i;
}
}
}
}
return -1;
}
public String getParameterType(String methodFqn, int index) {
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && index < md.parameters().size()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(index);
return svd.getType().toString();
}
}
return null;
}
public boolean isTypeCompatible(String actualType, String expectedType) {
if (actualType == null || expectedType == null) return true;
if (expectedType.equals("Object") || expectedType.equals("java.lang.Object")) return true;
if (actualType.equals("Object") || actualType.equals("java.lang.Object")) return true;
if (expectedType.length() == 1 && Character.isUpperCase(expectedType.charAt(0))) return true;
if (actualType.length() == 1 && Character.isUpperCase(actualType.charAt(0))) return true;
String originalExpectedType = expectedType;
if (actualType.contains("<")) {
String rawType = actualType.substring(0, actualType.indexOf('<'));
if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) {
int start = actualType.indexOf('<');
int end = actualType.lastIndexOf('>');
if (start >= 0 && end > start) {
actualType = actualType.substring(start + 1, end);
}
}
}
if (expectedType.contains("<")) {
String rawType = expectedType.substring(0, expectedType.indexOf('<'));
if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) {
int start = expectedType.indexOf('<');
int end = expectedType.lastIndexOf('>');
if (start >= 0 && end > start) {
expectedType = expectedType.substring(start + 1, end);
}
}
}
if (actualType.contains("<")) {
actualType = actualType.substring(0, actualType.indexOf('<'));
}
if (expectedType.contains("<")) {
expectedType = expectedType.substring(0, expectedType.indexOf('<'));
}
if (actualType.equals(expectedType)) return true;
if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true;
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType);
if (td != null) {
if (td instanceof TypeDeclaration typeDecl) {
String superFqn = context.getSuperclassFqn(typeDecl);
while (superFqn != null) {
if (superFqn.contains("<")) {
superFqn = superFqn.substring(0, superFqn.indexOf('<'));
}
if (superFqn.equals(expectedType) || superFqn.endsWith("." + expectedType) || expectedType.endsWith("." + superFqn)) return true;
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
superFqn = superTd != null ? context.getSuperclassFqn(superTd) : null;
}
}
List interfaces = java.util.Collections.emptyList();
if (td instanceof TypeDeclaration typeDecl) {
interfaces = typeDecl.superInterfaceTypes();
} else if (td instanceof EnumDeclaration enumDecl) {
interfaces = enumDecl.superInterfaceTypes();
} else if (td instanceof RecordDeclaration recordDecl) {
interfaces = recordDecl.superInterfaceTypes();
}
for (Object interfaceType : interfaces) {
String itStr = interfaceType.toString();
if (itStr.contains("<")) {
itStr = itStr.substring(0, itStr.indexOf('<'));
}
if (itStr.equals(expectedType) || itStr.endsWith("." + expectedType) || expectedType.endsWith("." + itStr)) return true;
}
}
if (originalExpectedType.contains("<")) {
int start = originalExpectedType.indexOf('<');
int end = originalExpectedType.lastIndexOf('>');
if (start >= 0 && end > start) {
String sub = originalExpectedType.substring(start + 1, end);
for (String part : sub.split(",")) {
String typeArg = part.trim();
if (isTypeCompatible(actualType, typeArg)) {
return true;
}
}
}
}
return false;
}
}

View File

@@ -0,0 +1,549 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class VariableTracer {
private final CodebaseContext context;
private final ConstantResolver constantResolver;
private ConstantExtractor constantExtractor;
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
this.context = context;
this.constantResolver = constantResolver;
this.dataFlowModel = new click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel(context);
}
public void setConstantExtractor(ConstantExtractor constantExtractor) {
this.constantExtractor = constantExtractor;
}
public Expression traceVariable(Expression expr) {
List<Expression> all = traceVariableAll(expr);
return all.isEmpty() ? expr : all.get(all.size() - 1);
}
public List<Expression> traceVariableAll(Expression expr) {
return dataFlowModel.getReachingDefinitions(expr);
}
public List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
return dataFlowModel.getReachingDefinitions(expr);
}
public Expression traceLocalSetter(String methodFqn, String varName, String getterName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
final Expression[] setterArg = new Expression[1];
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if (node.getName().getIdentifier().equalsIgnoreCase("set" + propName) || node.getName().getIdentifier().equalsIgnoreCase(propName)) {
if (node.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
if (!node.arguments().isEmpty()) {
setterArg[0] = (Expression) node.arguments().get(0);
}
}
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof FieldAccess fa) {
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
setterArg[0] = node.getRightHandSide();
}
}
} else if (node.getLeftHandSide() instanceof QualifiedName qqn) {
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
setterArg[0] = node.getRightHandSide();
}
}
}
return super.visit(node);
}
});
return setterArg[0];
}
}
return null;
}
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
if (methodFqn == null) return null;
int lastDot = methodFqn.lastIndexOf('.');
if (lastDot > 0) {
String fqn = methodFqn.substring(0, lastDot);
String methodName = methodFqn.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(fqn);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
if (md != null) {
for (Object pObj : md.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
if (svd.getName().getIdentifier().equals(cleanFieldName)) {
return svd.getType().toString();
}
}
final String[] foundType = new String[1];
if (md.getBody() != null) {
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationStatement node) {
for (Object fragObj : node.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
foundType[0] = node.getType().toString();
}
}
return super.visit(node);
}
@Override
public boolean visit(LambdaExpression node) {
for (Object paramObj : node.parameters()) {
VariableDeclaration param = (VariableDeclaration) paramObj;
if (param.getName().getIdentifier().equals(cleanFieldName)) {
if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
foundType[0] = svd.getType().toString();
return false;
}
ASTNode parent = node.getParent();
if (parent instanceof MethodInvocation mi) {
Expression caller = mi.getExpression();
boolean hasMap = false;
while (caller instanceof MethodInvocation chainMi) {
String mName = chainMi.getName().getIdentifier();
if (mName.equals("map") || mName.equals("flatMap")) {
hasMap = true;
if (chainMi.arguments().size() == 1 && chainMi.arguments().get(0) instanceof LambdaExpression lambda) {
if (lambda.getBody() instanceof MethodInvocation mapMi) {
Expression mapCaller = mapMi.getExpression();
if (mapCaller instanceof SimpleName mapSn) {
String mapCallerType = getVariableDeclaredType(methodFqn, mapSn.getIdentifier());
if (mapCallerType != null) {
TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
TypeDeclaration td = context.getTypeDeclaration(mapCallerType, cu);
if (td != null) {
MethodDeclaration mapMd = context.findMethodDeclaration(td, mapMi.getName().getIdentifier(), true);
if (mapMd != null && mapMd.getReturnType2() != null) {
foundType[0] = mapMd.getReturnType2().toString();
return false;
}
}
}
}
}
}
}
if (mName.equals("stream") || mName.equals("filter") || mName.equals("map") ||
mName.equals("flatMap") || mName.equals("peek") || mName.equals("distinct") ||
mName.equals("sorted") || mName.equals("limit") || mName.equals("skip")) {
caller = chainMi.getExpression();
} else {
break;
}
}
if (!hasMap) {
if (caller instanceof SimpleName callerSn) {
String callerName = callerSn.getIdentifier();
if (!callerName.equals(cleanFieldName)) {
String callerType = getVariableDeclaredType(methodFqn, callerName);
if (callerType != null && callerType.contains("<")) {
int start = callerType.indexOf('<');
int end = callerType.lastIndexOf('>');
if (start >= 0 && end > start) {
foundType[0] = callerType.substring(start + 1, end);
return false;
}
}
}
} else if (caller instanceof FieldAccess fa) {
String callerName = fa.getName().getIdentifier();
if (!callerName.equals(cleanFieldName)) {
String callerType = getVariableDeclaredType(methodFqn, callerName);
if (callerType != null && callerType.contains("<")) {
int start = callerType.indexOf('<');
int end = callerType.lastIndexOf('>');
if (start >= 0 && end > start) {
foundType[0] = callerType.substring(start + 1, end);
return false;
}
}
}
}
}
}
}
}
return super.visit(node);
}
});
}
if (foundType[0] != null) return foundType[0];
}
// Fallback to fields
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(cleanFieldName)) {
return fd.getType().toString();
}
}
}
}
}
// Final fallback: ask constantExtractor to resolve it via method return constant
if (constantExtractor != null && methodFqn.contains(".")) {
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
CompilationUnit cu = td != null && td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
List<String> values = constantExtractor.resolveMethodReturnConstant(cleanFieldName, methodName, 0, new HashSet<>(), cu);
if (values != null && !values.isEmpty()) {
// If it resolves to some class constant, we could infer type or return a fallback
return null;
}
}
return null;
}
public String traceLocalVariable(String methodFqn, String varName) {
if (methodFqn == null || !methodFqn.contains(".")) return null;
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
List<Expression> initializers = new ArrayList<>();
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
initializers.add(node.getInitializer());
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
initializers.add(node.getRightHandSide());
}
return super.visit(node);
}
});
if (!initializers.isEmpty()) {
List<String> stringified = new ArrayList<>();
for (Expression expr : initializers) {
Expression traced = traceVariable(expr);
if (traced instanceof MethodInvocation mi) {
Expression innerMost = unwrapMethodInvocation(mi, 0);
stringified.add(innerMost.toString());
} else if (traced instanceof ArrayInitializer) {
stringified.add("new Object[]" + traced.toString());
} else {
stringified.add(traced.toString());
}
}
if (stringified.size() == 1) return stringified.get(0);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < stringified.size() - 1; i++) {
sb.append("true ? ").append(stringified.get(i)).append(" : ");
}
sb.append(stringified.get(stringified.size() - 1));
return sb.toString();
}
}
// If local variable not found, check field assignments
String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName;
final Expression[] fieldInitializer = new Expression[1];
ASTVisitor fieldVisitor = new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) {
fieldInitializer[0] = node.getInitializer();
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
Expression left = node.getLeftHandSide();
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) ||
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) {
fieldInitializer[0] = node.getRightHandSide();
}
return super.visit(node);
}
};
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) {
fieldInitializer[0] = frag.getInitializer();
}
}
}
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
for (MethodDeclaration classMd : td.getMethods()) {
if (classMd.isConstructor() && classMd.getBody() != null) {
classMd.getBody().accept(fieldVisitor);
}
}
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
for (MethodDeclaration classMd : td.getMethods()) {
if (!classMd.isConstructor() && classMd.getBody() != null) {
classMd.getBody().accept(fieldVisitor);
}
}
if (fieldInitializer[0] != null) return fieldInitializer[0].toString();
}
return null;
}
public String traceLocalVariable(String methodFqn, String varName, Map<String, String> parameterValues) {
String result = traceLocalVariable(methodFqn, varName);
if (result != null && parameterValues != null && !parameterValues.isEmpty()) {
TypeDeclaration td = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
if (td != null) {
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
final ASTNode[] switchNode = new ASTNode[1];
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
Expression init = node.getInitializer();
if (init.getNodeType() == ASTNode.SWITCH_EXPRESSION) {
switchNode[0] = init;
return false;
}
if (init.getNodeType() == ASTNode.SWITCH_STATEMENT) {
switchNode[0] = init;
return false;
}
}
return super.visit(node);
}
@Override
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
Expression rhs = node.getRightHandSide();
if (rhs.getNodeType() == ASTNode.SWITCH_EXPRESSION) {
switchNode[0] = rhs;
return false;
}
if (rhs.getNodeType() == ASTNode.SWITCH_STATEMENT) {
switchNode[0] = rhs;
return false;
}
}
return super.visit(node);
}
});
if (switchNode[0] != null) {
if (switchNode[0] instanceof SwitchExpression se) {
String evaluated = constantResolver.evaluateSwitchWithParams(se, parameterValues, context);
if (evaluated != null) return evaluated;
} else if (switchNode[0] instanceof SwitchStatement ss) {
String evaluated = constantResolver.evaluateSwitchWithParams(ss, parameterValues, context);
if (evaluated != null) return evaluated;
}
}
}
}
}
return result;
}
public Map<String, String> buildParameterValuesMap(String caller, String target, Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
Map<String, String> paramValues = new HashMap<>();
List<CallEdge> edges = callGraph.get(caller);
if (edges == null) {
return paramValues;
}
// We also want to support heuristic matches using a helper if isHeuristicMatch is not directly available, but let's check
// We can check if name equals or if it resolves to target
for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
List<String> args = edge.getArguments();
if (args == null || args.isEmpty()) break;
int lastDot = target.lastIndexOf('.');
if (lastDot < 0) break;
String className = target.substring(0, lastDot);
String methodName = target.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) break;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null) break;
List<String> paramNames = new ArrayList<>();
for (Object paramObj : md.parameters()) {
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
paramNames.add(param.getName().getIdentifier());
}
int minSize = Math.min(paramNames.size(), args.size());
for (int j = 0; j < minSize; j++) {
String argValue = args.get(j);
String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph);
paramValues.put(paramNames.get(j), resolvedArgValue);
}
break;
}
}
return paramValues;
}
public String resolveArgumentValue(String argValue, String callerMethod, List<String> path, int pathIndex, Map<String, List<CallEdge>> callGraph) {
if (argValue == null) return null;
if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*")) {
return argValue;
}
int callerIndex = path.indexOf(callerMethod);
if (callerIndex <= 0) return argValue;
String prevCaller = path.get(callerIndex - 1);
List<CallEdge> prevEdges = callGraph.get(prevCaller);
if (prevEdges == null) return argValue;
for (CallEdge edge : prevEdges) {
if (edge.getTargetMethod().equals(callerMethod) || isHeuristicMatch(edge.getTargetMethod(), callerMethod)) {
List<String> prevArgs = edge.getArguments();
if (prevArgs == null || prevArgs.isEmpty()) break;
int lastDot = callerMethod.lastIndexOf('.');
if (lastDot < 0) break;
String className = callerMethod.substring(0, lastDot);
String methodName = callerMethod.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(className);
if (td == null) break;
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md == null) break;
List<String> paramNames = new ArrayList<>();
for (Object paramObj : md.parameters()) {
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
paramNames.add(param.getName().getIdentifier());
}
int paramIdx = paramNames.indexOf(argValue);
if (paramIdx >= 0 && paramIdx < prevArgs.size()) {
String prevArg = prevArgs.get(paramIdx);
return resolveArgumentValue(prevArg, prevCaller, path, pathIndex, callGraph);
}
break;
}
}
return argValue;
}
private boolean isHeuristicMatch(String neighbor, String target) {
if (neighbor == null || target == null) return false;
if (neighbor.equals(target)) return true;
if (neighbor.contains(".") && target.contains(".")) {
String methodNeighbor = neighbor.substring(neighbor.lastIndexOf('.') + 1);
String methodTarget = target.substring(target.lastIndexOf('.') + 1);
if (!methodNeighbor.equals(methodTarget)) return false;
String classNeighbor = neighbor.substring(0, neighbor.lastIndexOf('.'));
String classTarget = target.substring(0, target.lastIndexOf('.'));
if (classNeighbor.equals(classTarget)) return true;
String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
List<String> impls = context.getImplementations(classTarget);
if (impls != null && impls.contains(classNeighbor)) return true;
List<String> implsN = context.getImplementations(classNeighbor);
if (implsN != null && implsN.contains(classTarget)) return true;
}
return false;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
return (MethodDeclaration) parent;
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
if (depth > 5) return mi;
if (mi.getExpression() != null) {
String exprStr = mi.getExpression().toString();
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) {
return mi;
}
}
if (!mi.arguments().isEmpty()) {
String mName = mi.getName().getIdentifier();
String lowerName = mName.toLowerCase();
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
lowerName.startsWith("create") || lowerName.startsWith("from") ||
lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
lowerName.startsWith("extract") || lowerName.startsWith("get") ||
lowerName.startsWith("find")) {
return mi;
}
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof MethodInvocation innerMi) {
return unwrapMethodInvocation(innerMi, depth + 1);
}
return arg;
}
return mi;
}
}

View File

@@ -0,0 +1,93 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import java.util.List;
public class InjectionPointAnalyzer {
private final SpringDependencyResolver resolver;
public InjectionPointAnalyzer(SpringDependencyResolver resolver) {
this.resolver = resolver;
}
/**
* Resolves the Spring bean injected into the given variable.
*
* @param variableBinding The binding of the field or parameter being injected.
* @return The FQN of the resolved concrete bean class, or null if unresolved.
*/
public String resolveInjectedBeanFqn(IVariableBinding variableBinding) {
if (variableBinding == null || variableBinding.getType() == null) {
return null;
}
String requiredTypeFqn = variableBinding.getType().getQualifiedName();
String injectionName = variableBinding.getName();
String qualifier = extractQualifier(variableBinding);
List<SpringBean> resolvedBeans = resolver.resolve(requiredTypeFqn, qualifier, injectionName);
if (resolvedBeans != null && resolvedBeans.size() == 1) {
return resolvedBeans.get(0).getTypeFqn();
}
return null;
}
private String extractQualifier(IVariableBinding binding) {
String qualifier = getQualifierValue(binding.getAnnotations());
if (qualifier != null) {
return qualifier;
}
if (binding.isField()) {
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = binding.getDeclaringClass();
if (declaringClass != null) {
for (org.eclipse.jdt.core.dom.IMethodBinding method : declaringClass.getDeclaredMethods()) {
boolean isAutowiredMethod = false;
for (IAnnotationBinding ann : method.getAnnotations()) {
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Autowired".equals(ann.getAnnotationType().getQualifiedName())) {
isAutowiredMethod = true;
break;
}
}
if (method.isConstructor() || isAutowiredMethod) {
for (int i = 0; i < method.getParameterTypes().length; i++) {
org.eclipse.jdt.core.dom.ITypeBinding paramType = method.getParameterTypes()[i];
try {
IAnnotationBinding[] paramAnns = method.getParameterAnnotations(i);
String paramQual = getQualifierValue(paramAnns);
if (paramQual != null && paramType.getErasure().isEqualTo(binding.getType().getErasure())) {
// For setters, verify it roughly matches the field name or just rely on type.
// We will rely on type equality for now as a heuristic.
return paramQual;
}
} catch (Exception e) {
// Ignore
}
}
}
}
}
}
return null;
}
private String getQualifierValue(IAnnotationBinding[] annotations) {
for (IAnnotationBinding ann : annotations) {
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
return (String) pair.getValue();
}
}
}
}
return null;
}
}

View File

@@ -0,0 +1,27 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import java.util.HashSet;
import java.util.Set;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class SpringBean {
private String typeFqn;
@Builder.Default
private Set<String> assignableTypes = new HashSet<>();
@Builder.Default
private Set<String> beanNames = new HashSet<>();
@Builder.Default
private Set<String> qualifiers = new HashSet<>();
private boolean isPrimary;
private Integer order;
private String declaringClassFqn;
private String factoryMethodName;
}

View File

@@ -0,0 +1,17 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SpringBeanRegistry {
private final List<SpringBean> beans = new ArrayList<>();
public void addBean(SpringBean bean) {
beans.add(bean);
}
public List<SpringBean> getBeans() {
return Collections.unmodifiableList(beans);
}
}

View File

@@ -0,0 +1,235 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import org.eclipse.jdt.core.dom.*;
import java.util.HashSet;
import java.util.Set;
public class SpringContextScanner extends ASTVisitor {
private final SpringBeanRegistry registry;
public SpringContextScanner(SpringBeanRegistry registry) {
this.registry = registry;
}
@Override
public boolean visit(TypeDeclaration node) {
ITypeBinding typeBinding = node.resolveBinding();
if (typeBinding == null) return true;
if (isSpringComponent(typeBinding)) {
SpringBean bean = SpringBean.builder()
.typeFqn(typeBinding.getQualifiedName())
.assignableTypes(getAssignableTypes(typeBinding))
.isPrimary(hasAnnotation(typeBinding, "org.springframework.context.annotation.Primary"))
.qualifiers(extractQualifiers(typeBinding))
.order(extractOrder(typeBinding))
.declaringClassFqn(typeBinding.getQualifiedName())
.build();
// Default bean name is uncapitalized simple name
String defaultName = Character.toLowerCase(node.getName().getIdentifier().charAt(0)) + node.getName().getIdentifier().substring(1);
// Check if @Component or @Service specifies a name (e.g., @Service("myService"))
String explicitName = extractStereotypeValue(typeBinding);
if (explicitName != null && !explicitName.isEmpty()) {
bean.getBeanNames().add(explicitName);
} else {
bean.getBeanNames().add(defaultName);
}
registry.addBean(bean);
}
return true;
}
@Override
public boolean visit(MethodDeclaration node) {
IMethodBinding methodBinding = node.resolveBinding();
if (methodBinding == null) return true;
if (hasAnnotation(methodBinding, "org.springframework.context.annotation.Bean")) {
ITypeBinding returnType = methodBinding.getReturnType();
if (returnType != null) {
Set<String> assignables = getAssignableTypes(returnType);
String[] concreteType = new String[] { returnType.getQualifiedName() };
if (node.getBody() != null) {
node.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement ret) {
if (ret.getExpression() != null) {
ITypeBinding exprType = ret.getExpression().resolveTypeBinding();
if (exprType != null && exprType.isClass()) {
concreteType[0] = exprType.getQualifiedName();
assignables.addAll(getAssignableTypes(exprType));
}
}
return super.visit(ret);
}
});
}
SpringBean bean = SpringBean.builder()
.typeFqn(concreteType[0])
.assignableTypes(assignables)
.isPrimary(hasAnnotation(methodBinding, "org.springframework.context.annotation.Primary"))
.qualifiers(extractQualifiers(methodBinding))
.order(extractOrder(methodBinding))
.declaringClassFqn(methodBinding.getDeclaringClass().getQualifiedName())
.factoryMethodName(node.getName().getIdentifier())
.build();
// Default bean name is method name
String defaultName = node.getName().getIdentifier();
// Check if @Bean specifies a name (e.g., @Bean("myBean"))
String explicitName = extractBeanName(methodBinding);
if (explicitName != null && !explicitName.isEmpty()) {
bean.getBeanNames().add(explicitName);
} else {
bean.getBeanNames().add(defaultName);
}
registry.addBean(bean);
}
}
return true;
}
private boolean isSpringComponent(ITypeBinding binding) {
return hasMetaAnnotation(binding, "org.springframework.stereotype.Component") ||
hasMetaAnnotation(binding, "org.springframework.web.bind.annotation.RestController");
}
private boolean hasMetaAnnotation(ITypeBinding binding, String targetFqn) {
return checkMetaAnnotation(binding.getAnnotations(), targetFqn, new HashSet<>());
}
private boolean checkMetaAnnotation(IAnnotationBinding[] annotations, String targetFqn, Set<String> visited) {
for (IAnnotationBinding ann : annotations) {
ITypeBinding annType = ann.getAnnotationType();
if (annType != null) {
String fqn = annType.getQualifiedName();
if (fqn.equals(targetFqn)) return true;
// Avoid circular meta-annotations (e.g. Documented -> Documented)
if (visited.add(fqn)) {
if (checkMetaAnnotation(annType.getAnnotations(), targetFqn, visited)) {
return true;
}
}
}
}
return false;
}
private boolean hasAnnotation(IBinding binding, String annotationFqn) {
IAnnotationBinding[] annotations = binding.getAnnotations();
for (IAnnotationBinding ann : annotations) {
if (ann.getAnnotationType() != null && annotationFqn.equals(ann.getAnnotationType().getQualifiedName())) {
return true;
}
}
return false;
}
private Set<String> getAssignableTypes(ITypeBinding binding) {
Set<String> types = new HashSet<>();
collectAssignableTypes(binding, types);
return types;
}
private void collectAssignableTypes(ITypeBinding binding, Set<String> types) {
if (binding == null) return;
String qName = binding.getQualifiedName();
if (qName != null && !qName.isEmpty()) {
types.add(qName);
}
ITypeBinding erasure = binding.getErasure();
if (erasure != null) {
String erasureName = erasure.getQualifiedName();
if (erasureName != null && !erasureName.isEmpty()) {
types.add(erasureName);
}
}
collectAssignableTypes(binding.getSuperclass(), types);
if (binding.getInterfaces() != null) {
for (ITypeBinding iface : binding.getInterfaces()) {
collectAssignableTypes(iface, types);
}
}
}
private Set<String> extractQualifiers(IBinding binding) {
Set<String> qualifiers = new HashSet<>();
for (IAnnotationBinding ann : binding.getAnnotations()) {
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
qualifiers.add((String) pair.getValue());
}
}
}
}
return qualifiers;
}
private Integer extractOrder(IBinding binding) {
for (IAnnotationBinding ann : binding.getAnnotations()) {
if (ann.getAnnotationType() != null && "org.springframework.core.annotation.Order".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if ("value".equals(pair.getName())) {
Object val = pair.getValue();
if (val instanceof Integer) {
return (Integer) val;
} else if (val instanceof IVariableBinding) {
Object constantValue = ((IVariableBinding) val).getConstantValue();
if (constantValue instanceof Integer) {
return (Integer) constantValue;
}
}
}
}
return Integer.MAX_VALUE; // Spring's default for @Order()
}
}
return null;
}
private String extractStereotypeValue(ITypeBinding binding) {
for (IAnnotationBinding ann : binding.getAnnotations()) {
// Check if annotation itself is meta-annotated with @Component
if (checkMetaAnnotation(new IAnnotationBinding[]{ann}, "org.springframework.stereotype.Component", new HashSet<>()) ||
"org.springframework.stereotype.Component".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
return (String) pair.getValue();
}
}
}
}
return null;
}
private String extractBeanName(IMethodBinding binding) {
for (IAnnotationBinding ann : binding.getAnnotations()) {
if ("org.springframework.context.annotation.Bean".equals(ann.getAnnotationType().getQualifiedName())) {
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof String) {
return (String) pair.getValue();
} else if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof Object[]) {
Object[] vals = (Object[]) pair.getValue();
if (vals.length > 0 && vals[0] instanceof String) {
return (String) vals[0];
}
}
}
}
}
return null;
}
}

View File

@@ -0,0 +1,87 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SpringDependencyResolver {
private final SpringBeanRegistry registry;
public SpringDependencyResolver(SpringBeanRegistry registry) {
this.registry = registry;
}
/**
* Resolves the exact SpringBean that would be injected for a given injection point.
*
* @param requiredTypeFqn The exact FQN of the type requested at the injection point.
* @param qualifier The value of the @Qualifier annotation on the injection point (if any).
* @param injectionName The name of the field or parameter being injected (used as fallback).
* @return A list of resolved beans. Usually 1. If 0, unresolved. If > 1, ambiguous.
*/
public List<SpringBean> resolve(String requiredTypeFqn, String qualifier, String injectionName) {
if (requiredTypeFqn == null) return new ArrayList<>();
// 1. Filter by Type (Exact FQN or Assignable Type)
System.out.println("RESOLVING " + requiredTypeFqn + " qual=" + qualifier + " injectionName=" + injectionName);
List<SpringBean> candidates = registry.getBeans().stream()
.filter(bean -> requiredTypeFqn.equals(bean.getTypeFqn()) || bean.getAssignableTypes().contains(requiredTypeFqn))
.collect(Collectors.toList());
System.out.println("CANDIDATES AFTER TYPE: " + candidates.stream().map(c -> c.getTypeFqn() + " (primary=" + c.isPrimary() + " names=" + c.getBeanNames() + " qual=" + c.getQualifiers() + ")").collect(Collectors.toList()));
if (candidates.isEmpty() || candidates.size() == 1) {
System.out.println("RETURNING: " + candidates.size());
return candidates;
}
// 2. Filter by Qualifier (if provided)
if (qualifier != null && !qualifier.isEmpty()) {
List<SpringBean> qualifiedCandidates = candidates.stream()
.filter(bean -> bean.getQualifiers().contains(qualifier) || bean.getBeanNames().contains(qualifier))
.collect(Collectors.toList());
if (qualifiedCandidates.size() == 1) {
return qualifiedCandidates;
} else if (!qualifiedCandidates.isEmpty()) {
candidates = qualifiedCandidates;
}
}
// 3. Filter by @Primary
List<SpringBean> primaryCandidates = candidates.stream()
.filter(SpringBean::isPrimary)
.collect(Collectors.toList());
if (primaryCandidates.size() == 1) {
return primaryCandidates;
}
// 4. Fallback to Injection Name (field name or parameter name)
if (injectionName != null && !injectionName.isEmpty()) {
List<SpringBean> namedCandidates = candidates.stream()
.filter(bean -> bean.getBeanNames().contains(injectionName))
.collect(Collectors.toList());
if (namedCandidates.size() == 1) {
return namedCandidates;
}
}
// 5. Fallback to @Order (lowest order wins)
Integer minOrder = candidates.stream()
.map(SpringBean::getOrder)
.filter(o -> o != null)
.min(Integer::compareTo)
.orElse(null);
if (minOrder != null) {
List<SpringBean> orderedCandidates = candidates.stream()
.filter(bean -> bean.getOrder() != null && bean.getOrder().equals(minOrder))
.collect(Collectors.toList());
if (!orderedCandidates.isEmpty()) {
candidates = orderedCandidates;
}
}
// Return whatever candidates remain (could be ambiguous)
System.out.println("RETURNING: " + candidates.size());
return candidates;
}
}

View File

@@ -292,6 +292,15 @@ public class AstTransitionParser {
// Try same file
ASTNode declNode = cu.findDeclaringNode(binding.getKey());
if (declNode instanceof MethodDeclaration md) {
if (md.getBody() != null && md.getBody().statements().size() == 1) {
Object stmt = md.getBody().statements().get(0);
if (stmt instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) {
Expression retExpr = rs.getExpression();
if (isLambdaOrAnonymous(retExpr)) {
return retExpr.toString();
}
}
}
return md.toString();
}

View File

@@ -0,0 +1,620 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.model.TransitionType;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class StateMachineAdapterParser {
private final CodebaseContext context;
private enum AnalysisGoal {TRANSITIONS, STATES}
public record AdapterStates(Set<String> initialStates, Set<String> endStates) {}
public record AdapterConfig(
List<Transition> transitions,
Set<String> initialStates,
Set<String> endStates
) {}
public StateMachineAdapterParser(CodebaseContext context) {
this.context = context;
}
public AdapterConfig parse(TypeDeclaration startClass) {
List<Transition> transitions = parseTransitions(startClass);
AdapterStates states = parseStates(startClass);
return new AdapterConfig(transitions, states.initialStates(), states.endStates());
}
public List<Transition> parseTransitions(TypeDeclaration startClass) {
List<Transition> allTransitions = new ArrayList<>();
Set<MethodVisit> visitedMethods = new HashSet<>();
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
return allTransitions;
}
public AdapterStates parseStates(TypeDeclaration startClass) {
Set<String> initialStates = new HashSet<>();
Set<String> endStates = new HashSet<>();
Set<MethodVisit> visitedMethods = new HashSet<>();
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
return new AdapterStates(initialStates, endStates);
}
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null)
return;
String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureTransitionsMethod(method)) {
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
}
}
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
// Hierarchy - Superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
if (superclassTd != null) {
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
}
}
// Hierarchy - Interfaces
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
if (interfaceTypeObj instanceof Type interfaceType) {
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
if (interfaceTd != null) {
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
}
}
}
}
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
}
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
if (visitedMethods.contains(visit)) {
return new ArrayList<>();
}
visitedMethods.add(visit);
Block body = method.getBody();
if (body == null) {
return new ArrayList<>();
}
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
}
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
List<Transition> transitions = new ArrayList<>();
if (expr instanceof MethodInvocation mi) {
if (isTransitionChainEntry(mi)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
} else if (isTransitionChainContinuation(mi, argsMap)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
} else if (isForEach(mi)) {
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
} else {
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
Expression lambdaReceiver = mi.getExpression();
if (lambdaReceiver instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof LambdaExpression lambda) {
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
}
}
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod == null) {
// Try to resolve method in another class if it's a static call or on a known type
Expression miReceiver = mi.getExpression();
if (miReceiver instanceof SimpleName sn) {
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
if (otherTd != null) {
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
if (calledMethod != null) {
// For calls to other classes, we switch the "searchStartClass" to that class
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
return transitions; // Handled
}
}
}
}
if (calledMethod != null) {
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
}
}
}
return transitions;
}
private boolean isForEach(MethodInvocation mi) {
return mi.getName().getIdentifier().equals("forEach");
}
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
List<?> params = lambda.parameters();
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
Object p = params.get(i);
String paramName = "";
if (p instanceof VariableDeclaration vp)
paramName = vp.getName().getIdentifier();
if (!paramName.isEmpty()) {
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
}
}
if (goal == AnalysisGoal.TRANSITIONS) {
if (lambda.getBody() instanceof Block block)
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
} else {
if (lambda.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
}
return Collections.emptyList();
}
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
List<Transition> transitions = new ArrayList<>();
Expression receiver = forEachMi.getExpression();
List<Expression> elements = unrollCollection(receiver, argsMap);
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
return transitions;
Object lambdaObj = forEachMi.arguments().get(0);
if (lambdaObj instanceof LambdaExpression lambda) {
String paramName = getLambdaParamName(lambda);
for (Expression element : elements) {
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
if (!paramName.isEmpty())
lambdaArgsMap.put(paramName, element);
if (goal == AnalysisGoal.TRANSITIONS) {
if (lambda.getBody() instanceof Block block)
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
else if (lambda.getBody() instanceof Expression lambdaExpr)
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
} else {
if (lambda.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
}
}
}
return transitions;
}
@SuppressWarnings("unchecked")
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
List<Transition> transitions = new ArrayList<>();
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
String paramName = efs.getParameter().getName().getIdentifier();
for (Expression element : elements) {
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
loopArgsMap.put(paramName, element);
if (goal == AnalysisGoal.TRANSITIONS) {
if (efs.getBody() instanceof Block block)
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
else if (efs.getBody() instanceof ExpressionStatement es)
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
} else {
if (efs.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
else if (efs.getBody() instanceof ExpressionStatement es)
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
}
}
return transitions;
}
@SuppressWarnings("unchecked")
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
Set<Expression> visited = new HashSet<>();
Expression current = expr;
while (current != null) {
if (!visited.add(current))
break;
if (current instanceof MethodInvocation mi) {
String name = mi.getName().getIdentifier();
Expression receiver = mi.getExpression();
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
if (mi.arguments().size() == 1) {
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list)
return (List<Expression>) list;
}
}
return (List<Expression>) mi.arguments();
}
}
if (current instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list)
return (List<Expression>) list;
if (resolved instanceof Expression e) {
current = e;
continue;
}
}
if (current instanceof ArrayInitializer ai)
return (List<Expression>) ai.expressions();
break;
}
return Collections.emptyList();
}
private String getLambdaParamName(LambdaExpression lambda) {
if (lambda.parameters().size() == 1) {
Object p = lambda.parameters().get(0);
if (p instanceof VariableDeclaration vp)
return vp.getName().getIdentifier();
}
return "";
}
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
List<Transition> transitions = new ArrayList<>();
for (Object stmt : block.statements()) {
if (stmt instanceof ExpressionStatement es)
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer != null) {
Object resolved = resolve(initializer, argsMap);
if (resolved != null)
argsMap.put(fragment.getName().getIdentifier(), resolved);
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
}
}
}
} else if (stmt instanceof EnhancedForStatement efs)
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
else if (stmt instanceof TryStatement ts)
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
}
return transitions;
}
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
for (Object stmt : block.statements()) {
if (stmt instanceof ExpressionStatement es)
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer != null) {
Object resolved = resolve(initializer, argsMap);
if (resolved != null)
argsMap.put(fragment.getName().getIdentifier(), resolved);
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
}
} else if (stmt instanceof EnhancedForStatement efs)
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
else if (stmt instanceof TryStatement ts)
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (expr instanceof MethodInvocation mi) {
if (isWithStatesChain(mi, argsMap))
parseStateChain(mi, initialStates, endStates, argsMap);
else if (isForEach(mi))
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
else {
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
Expression receiver = mi.getExpression();
if (receiver instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof LambdaExpression lambda) {
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
}
}
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod != null) {
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
}
}
}
}
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
Map<String, Object> map = new HashMap<>(currentArgsMap);
List<?> params = method.parameters();
List<?> args = call.arguments();
for (int i = 0; i < params.size(); i++) {
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
String paramName = param.getName().getIdentifier();
if (param.isVarargs() && i < params.size()) {
List<Expression> varargList = new ArrayList<>();
for (int j = i; j < args.size(); j++) {
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
if (resolved instanceof List<?> list) {
for (Object item : list) {
if (item instanceof Expression e)
varargList.add(e);
}
} else if (resolved instanceof Expression e) {
varargList.add(e);
}
}
map.put(paramName, varargList);
} else if (i < args.size()) {
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
}
}
return map;
}
private Object resolve(Expression expr, Map<String, Object> argsMap) {
if (expr instanceof NullLiteral)
return null;
Set<Expression> visited = new HashSet<>();
Object current = expr;
while (current instanceof SimpleName sn) {
if (!visited.add((SimpleName) current))
break;
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved == null || resolved == current)
break;
current = resolved;
}
return current;
}
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
Object resolved = resolve(expr, argsMap);
if (resolved instanceof Expression e)
return e;
return expr;
}
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
Map<String, Expression> map = new HashMap<>();
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
if (entry.getValue() instanceof Expression e)
map.put(entry.getKey(), e);
}
return map;
}
private boolean isTransitionChainEntry(MethodInvocation mi) {
Expression current = mi;
while (current instanceof MethodInvocation call) {
String name = call.getName().getIdentifier();
if (TransitionType.fromMethodName(name).isPresent())
return true;
current = call.getExpression();
}
return false;
}
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
String name = call.getName().getIdentifier();
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) {
current = next;
continue;
}
Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
return resolved == null; // null means it's a parameter we track
}
if (TransitionType.fromMethodName(name).isPresent())
return true;
break;
}
return false;
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
return findMethodInHierarchy(methodName, td, new HashSet<>());
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
if (td == null)
return null;
String fqn = context.getFqn(td);
if (visited.contains(fqn))
return null;
visited.add(fqn);
for (MethodDeclaration method : td.getMethods())
if (method.getName().getIdentifier().equals(methodName))
return method;
Type superclassType = td.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
return findMethodInHierarchy(methodName, superclassTd, visited);
}
return null;
}
private boolean isRelevantMethod(MethodDeclaration method) {
return true;
}
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods())
if (isConfigureTransitionsMethod(method))
return method;
return null;
}
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
return false;
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
}
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null)
return;
String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureStatesMethod(method)) {
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
// Hierarchy - Superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
if (superclassTd != null)
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
}
// Hierarchy - Interfaces
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
if (interfaceTypeObj instanceof Type interfaceType) {
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
if (interfaceTd != null)
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
}
}
}
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
if (visitedMethods.contains(visit))
return;
visitedMethods.add(visit);
Block body = method.getBody();
if (body == null)
return;
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
if (call.getName().getIdentifier().equals("withStates"))
return true;
Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) {
current = next;
continue;
}
Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
break;
}
return false;
}
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
List<MethodInvocation> chain = new ArrayList<>();
Expression current = mi;
while (current instanceof MethodInvocation m) {
chain.addFirst(m);
current = m.getExpression();
}
CompilationUnit cu = (CompilationUnit) mi.getRoot();
for (MethodInvocation call : chain) {
String methodName = call.getName().getIdentifier();
if (call.arguments().isEmpty())
continue;
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
if ("initial".equals(methodName)) {
if (!isInsideRegionOrFork(call)) {
initialStates.add(context.resolveState(arg, cu).toString());
}
} else if ("end".equals(methodName)) {
endStates.add(context.resolveState(arg, cu).toString());
}
}
}
private boolean isInsideRegionOrFork(MethodInvocation mi) {
Expression expr = mi.getExpression();
while (expr instanceof MethodInvocation parent) {
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
return true;
expr = parent.getExpression();
}
return false;
}
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods())
if (isConfigureStatesMethod(method))
return method;
return null;
}
private boolean isConfigureStatesMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
return false;
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
}
private String getSimpleName(Type type) {
if (type.isSimpleType())
return ((SimpleType) type).getName().getFullyQualifiedName();
if (type.isParameterizedType())
return getSimpleName(((ParameterizedType) type).getType());
return type.toString();
}
}

View File

@@ -1,632 +1,37 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.model.TransitionType;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.Getter;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NullLiteral;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class StateMachineAggregator {
private final CodebaseContext context;
private enum AnalysisGoal {TRANSITIONS, STATES}
public StateMachineAggregator(CodebaseContext context) {
this.context = context;
}
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
List<Transition> allTransitions = new ArrayList<>();
Set<MethodVisit> visitedMethods = new HashSet<>();
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
return allTransitions;
}
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null)
return;
String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureTransitionsMethod(method)) {
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
}
}
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
// Hierarchy - Superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
if (superclassTd != null) {
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
}
}
// Hierarchy - Interfaces
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
if (interfaceTypeObj instanceof Type interfaceType) {
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
if (interfaceTd != null) {
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
}
}
}
}
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
}
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
if (visitedMethods.contains(visit)) {
return new ArrayList<>();
}
visitedMethods.add(visit);
Block body = method.getBody();
if (body == null) {
return new ArrayList<>();
}
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
}
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
List<Transition> transitions = new ArrayList<>();
if (expr instanceof MethodInvocation mi) {
if (isTransitionChainEntry(mi)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
} else if (isTransitionChainContinuation(mi, argsMap)) {
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
} else if (isForEach(mi)) {
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
} else {
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
Expression lambdaReceiver = mi.getExpression();
if (lambdaReceiver instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof LambdaExpression lambda) {
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
}
}
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod == null) {
// Try to resolve method in another class if it's a static call or on a known type
Expression miReceiver = mi.getExpression();
if (miReceiver instanceof SimpleName sn) {
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
if (otherTd != null) {
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
if (calledMethod != null) {
// For calls to other classes, we switch the "searchStartClass" to that class
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
return transitions; // Handled
}
}
}
}
if (calledMethod != null) {
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
}
}
}
return transitions;
}
private boolean isForEach(MethodInvocation mi) {
return mi.getName().getIdentifier().equals("forEach");
}
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
List<?> params = lambda.parameters();
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
Object p = params.get(i);
String paramName = "";
if (p instanceof VariableDeclaration vp)
paramName = vp.getName().getIdentifier();
if (!paramName.isEmpty()) {
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
}
}
if (goal == AnalysisGoal.TRANSITIONS) {
if (lambda.getBody() instanceof Block block)
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
} else {
if (lambda.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
}
return Collections.emptyList();
}
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
List<Transition> transitions = new ArrayList<>();
Expression receiver = forEachMi.getExpression();
List<Expression> elements = unrollCollection(receiver, argsMap);
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
return transitions;
Object lambdaObj = forEachMi.arguments().get(0);
if (lambdaObj instanceof LambdaExpression lambda) {
String paramName = getLambdaParamName(lambda);
for (Expression element : elements) {
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
if (!paramName.isEmpty())
lambdaArgsMap.put(paramName, element);
if (goal == AnalysisGoal.TRANSITIONS) {
if (lambda.getBody() instanceof Block block)
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
else if (lambda.getBody() instanceof Expression lambdaExpr)
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
} else {
if (lambda.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
else if (lambda.getBody() instanceof Expression lambdaExpr)
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
}
}
}
return transitions;
}
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
List<Transition> transitions = new ArrayList<>();
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
String paramName = efs.getParameter().getName().getIdentifier();
for (Expression element : elements) {
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
loopArgsMap.put(paramName, element);
if (goal == AnalysisGoal.TRANSITIONS) {
if (efs.getBody() instanceof Block block)
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
else if (efs.getBody() instanceof ExpressionStatement es)
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
} else {
if (efs.getBody() instanceof Block block)
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
else if (efs.getBody() instanceof ExpressionStatement es)
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
}
}
return transitions;
}
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
Set<Expression> visited = new HashSet<>();
Expression current = expr;
while (current != null) {
if (!visited.add(current))
break;
if (current instanceof MethodInvocation mi) {
String name = mi.getName().getIdentifier();
Expression receiver = mi.getExpression();
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
if (mi.arguments().size() == 1) {
Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list)
return (List<Expression>) list;
}
}
return (List<Expression>) mi.arguments();
}
}
if (current instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof List list)
return (List<Expression>) list;
if (resolved instanceof Expression e) {
current = e;
continue;
}
}
if (current instanceof ArrayInitializer ai)
return (List<Expression>) ai.expressions();
break;
}
return Collections.emptyList();
}
private String getLambdaParamName(LambdaExpression lambda) {
if (lambda.parameters().size() == 1) {
Object p = lambda.parameters().get(0);
if (p instanceof VariableDeclaration vp)
return vp.getName().getIdentifier();
}
return "";
}
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
List<Transition> transitions = new ArrayList<>();
for (Object stmt : block.statements()) {
if (stmt instanceof ExpressionStatement es)
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer != null) {
Object resolved = resolve(initializer, argsMap);
if (resolved != null)
argsMap.put(fragment.getName().getIdentifier(), resolved);
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
}
}
}
} else if (stmt instanceof EnhancedForStatement efs)
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
else if (stmt instanceof TryStatement ts)
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
}
return transitions;
}
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
for (Object stmt : block.statements()) {
if (stmt instanceof ExpressionStatement es)
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
else if (stmt instanceof VariableDeclarationStatement vds) {
for (Object fragmentObj : vds.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer != null) {
Object resolved = resolve(initializer, argsMap);
if (resolved != null)
argsMap.put(fragment.getName().getIdentifier(), resolved);
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
}
} else if (stmt instanceof EnhancedForStatement efs)
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
else if (stmt instanceof TryStatement ts)
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (expr instanceof MethodInvocation mi) {
if (isWithStatesChain(mi, argsMap))
parseStateChain(mi, initialStates, endStates, argsMap);
else if (isForEach(mi))
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
else {
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
Expression receiver = mi.getExpression();
if (receiver instanceof SimpleName sn) {
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved instanceof LambdaExpression lambda) {
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
}
}
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
if (calledMethod != null) {
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
}
}
}
}
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
Map<String, Object> map = new HashMap<>(currentArgsMap);
List<?> params = method.parameters();
List<?> args = call.arguments();
for (int i = 0; i < params.size(); i++) {
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
String paramName = param.getName().getIdentifier();
if (param.isVarargs() && i < params.size()) {
List<Expression> varargList = new ArrayList<>();
for (int j = i; j < args.size(); j++) {
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
if (resolved instanceof List<?> list) {
for (Object item : list) {
if (item instanceof Expression e)
varargList.add(e);
}
} else if (resolved instanceof Expression e) {
varargList.add(e);
}
}
map.put(paramName, varargList);
} else if (i < args.size()) {
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
}
}
return map;
}
private Object resolve(Expression expr, Map<String, Object> argsMap) {
if (expr instanceof NullLiteral)
return null;
Set<Expression> visited = new HashSet<>();
Object current = expr;
while (current instanceof SimpleName sn) {
if (!visited.add((SimpleName) current))
break;
Object resolved = argsMap.get(sn.getIdentifier());
if (resolved == null || resolved == current)
break;
current = resolved;
}
return current;
}
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
Object resolved = resolve(expr, argsMap);
if (resolved instanceof Expression e)
return e;
return expr;
}
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
Map<String, Expression> map = new HashMap<>();
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
if (entry.getValue() instanceof Expression e)
map.put(entry.getKey(), e);
}
return map;
}
private boolean isTransitionChainEntry(MethodInvocation mi) {
Expression current = mi;
while (current instanceof MethodInvocation call) {
String name = call.getName().getIdentifier();
if (TransitionType.fromMethodName(name).isPresent())
return true;
current = call.getExpression();
}
return false;
}
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
String name = call.getName().getIdentifier();
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) {
current = next;
continue;
}
Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
return resolved == null; // null means it's a parameter we track
}
if (TransitionType.fromMethodName(name).isPresent())
return true;
break;
}
return false;
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
return findMethodInHierarchy(methodName, td, new HashSet<>());
}
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
if (td == null)
return null;
String fqn = context.getFqn(td);
if (visited.contains(fqn))
return null;
visited.add(fqn);
for (MethodDeclaration method : td.getMethods())
if (method.getName().getIdentifier().equals(methodName))
return method;
Type superclassType = td.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
return findMethodInHierarchy(methodName, superclassTd, visited);
}
return null;
}
private boolean isRelevantMethod(MethodDeclaration method) {
return true;
}
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods())
if (isConfigureTransitionsMethod(method))
return method;
return null;
}
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
return false;
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
}
private final StateMachineAdapterParser adapterParser;
@Getter
private final Set<String> initialStates = new HashSet<>();
@Getter
private final Set<String> endStates = new HashSet<>();
public StateMachineAggregator(CodebaseContext context) {
this.context = context;
this.adapterParser = new StateMachineAdapterParser(context);
}
public List<Transition> aggregateTransitions(TypeDeclaration startClass) {
return adapterParser.parseTransitions(startClass);
}
public void aggregateStates(TypeDeclaration startClass) {
Set<MethodVisit> visitedMethods = new HashSet<>();
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
}
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
if (currentTd == null)
return;
String fqn = context.getFqn(currentTd);
if (visitedClasses.contains(fqn))
return;
visitedClasses.add(fqn);
for (MethodDeclaration method : currentTd.getMethods()) {
if (isConfigureStatesMethod(method)) {
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
}
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
// Hierarchy - Superclass
Type superclassType = currentTd.getSuperclassType();
if (superclassType != null) {
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
if (superclassTd != null)
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
}
// Hierarchy - Interfaces
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
if (interfaceTypeObj instanceof Type interfaceType) {
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
if (interfaceTd != null)
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
}
}
}
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
if (visitedMethods.contains(visit))
return;
visitedMethods.add(visit);
Block body = method.getBody();
if (body == null)
return;
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
}
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
Expression current = mi;
Set<Expression> visited = new HashSet<>();
while (current instanceof MethodInvocation call) {
if (!visited.add(current))
break;
if (call.getName().getIdentifier().equals("withStates"))
return true;
Expression receiver = call.getExpression();
if (receiver instanceof MethodInvocation next) {
current = next;
continue;
}
Object resolved = resolve(receiver, argsMap);
if (resolved instanceof MethodInvocation next) {
current = next;
continue;
}
break;
}
return false;
}
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
List<MethodInvocation> chain = new ArrayList<>();
Expression current = mi;
while (current instanceof MethodInvocation m) {
chain.addFirst(m);
current = m.getExpression();
}
CompilationUnit cu = (CompilationUnit) mi.getRoot();
for (MethodInvocation call : chain) {
String methodName = call.getName().getIdentifier();
if (call.arguments().isEmpty())
continue;
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
if ("initial".equals(methodName)) {
if (!isInsideRegionOrFork(call)) {
initialStates.add(context.resolveState(arg, cu).toString());
}
} else if ("end".equals(methodName)) {
endStates.add(context.resolveState(arg, cu).toString());
}
}
}
private boolean isInsideRegionOrFork(MethodInvocation mi) {
Expression expr = mi.getExpression();
while (expr instanceof MethodInvocation parent) {
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
return true;
expr = parent.getExpression();
}
return false;
}
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
for (MethodDeclaration method : td.getMethods())
if (isConfigureStatesMethod(method))
return method;
return null;
}
private boolean isConfigureStatesMethod(MethodDeclaration method) {
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
return false;
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
}
private String getSimpleName(Type type) {
if (type.isSimpleType())
return ((SimpleType) type).getName().getFullyQualifiedName();
if (type.isParameterizedType())
return getSimpleName(((ParameterizedType) type).getType());
return type.toString();
StateMachineAdapterParser.AdapterStates states = adapterParser.parseStates(startClass);
this.initialStates.clear();
this.initialStates.addAll(states.initialStates());
this.endStates.clear();
this.endStates.addAll(states.endStates());
}
}

View File

@@ -36,4 +36,63 @@ public final class AstUtils {
return type.toString(); // fallback
}
}
public static boolean isFunctionalCollectionMethod(org.eclipse.jdt.core.dom.MethodInvocation node) {
if (node.getExpression() == null) {
return false;
}
org.eclipse.jdt.core.dom.ITypeBinding typeBinding = node.getExpression().resolveTypeBinding();
if (typeBinding != null) {
if (isCollectionOrStreamType(typeBinding)) {
return true;
}
return false;
}
String methodName = node.getName().getIdentifier();
if (methodName.equals("forEach") || methodName.equals("map") ||
methodName.equals("filter") || methodName.equals("flatMap") ||
methodName.equals("anyMatch") || methodName.equals("allMatch") ||
methodName.equals("noneMatch") || methodName.equals("reduce") ||
methodName.equals("collect") || methodName.equals("removeIf") ||
methodName.equals("ifPresent") || methodName.equals("ifPresentOrElse") ||
methodName.equals("compute") || methodName.equals("computeIfAbsent") ||
methodName.equals("computeIfPresent") || methodName.equals("replaceAll") ||
methodName.equals("merge") || methodName.equals("peek") ||
methodName.equals("doOnNext") || methodName.equals("doOnSuccess") ||
methodName.equals("doOnError") || methodName.equals("concatMap") ||
methodName.equals("switchMap")) {
return true;
}
return false;
}
private static boolean isCollectionOrStreamType(org.eclipse.jdt.core.dom.ITypeBinding typeBinding) {
if (typeBinding == null) return false;
String fqn = typeBinding.getErasure().getQualifiedName();
if (fqn.startsWith("java.util.stream.") ||
fqn.equals("java.lang.Iterable") ||
fqn.equals("java.util.Collection") ||
fqn.equals("java.util.List") ||
fqn.equals("java.util.Set") ||
fqn.equals("java.util.Map") ||
fqn.equals("java.util.Iterator") ||
fqn.equals("java.util.Optional") ||
fqn.equals("reactor.core.publisher.Flux") ||
fqn.equals("reactor.core.publisher.Mono")) {
return true;
}
for (org.eclipse.jdt.core.dom.ITypeBinding intf : typeBinding.getInterfaces()) {
if (isCollectionOrStreamType(intf)) return true;
}
if (typeBinding.getSuperclass() != null) {
return isCollectionOrStreamType(typeBinding.getSuperclass());
}
return false;
}
}

View File

@@ -0,0 +1,108 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.Set;
public final class BindingResolver {
private BindingResolver() {
// Prevent instantiation
}
/**
* Resolves the type binding for a given AST Type node.
*/
public static ITypeBinding resolveType(Type type) {
if (type == null) return null;
return type.resolveBinding();
}
/**
* Resolves the type binding for a given AST Expression node.
*/
public static ITypeBinding resolveType(Expression expression) {
if (expression == null) return null;
return expression.resolveTypeBinding();
}
/**
* Resolves the method binding for a MethodInvocation.
*/
public static IMethodBinding resolveMethod(MethodInvocation invocation) {
if (invocation == null) return null;
return invocation.resolveMethodBinding();
}
/**
* Resolves the constructor binding for a ClassInstanceCreation.
*/
public static IMethodBinding resolveConstructor(ClassInstanceCreation creation) {
if (creation == null) return null;
return creation.resolveConstructorBinding();
}
/**
* Resolves the constructor binding for a ConstructorInvocation (e.g. this(...)).
*/
public static IMethodBinding resolveConstructor(ConstructorInvocation invocation) {
if (invocation == null) return null;
return invocation.resolveConstructorBinding();
}
/**
* Resolves the constructor binding for a SuperConstructorInvocation (e.g. super(...)).
*/
public static IMethodBinding resolveConstructor(SuperConstructorInvocation invocation) {
if (invocation == null) return null;
return invocation.resolveConstructorBinding();
}
/**
* Resolves the type binding of a TypeDeclaration.
*/
public static ITypeBinding resolveType(TypeDeclaration td) {
if (td == null) return null;
return td.resolveBinding();
}
/**
* Gets the fully qualified name of a type binding, safely handling nulls and erasure.
*/
public static String getFullyQualifiedName(ITypeBinding binding) {
if (binding == null) return null;
return binding.getErasure().getQualifiedName();
}
public static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn) {
return isSubtypeOf(childBinding, targetFqn, new java.util.HashSet<>());
}
private static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn, Set<String> visited) {
if (childBinding == null || targetFqn == null) return false;
String key = childBinding.getKey();
if (key != null && !visited.add(key)) {
return false;
}
// Direct match
if (targetFqn.equals(childBinding.getErasure().getQualifiedName())) {
return true;
}
// Check interfaces
for (ITypeBinding intf : childBinding.getInterfaces()) {
if (isSubtypeOf(intf, targetFqn, visited)) {
return true;
}
}
// Check superclass
ITypeBinding superclass = childBinding.getSuperclass();
if (superclass != null) {
return isSubtypeOf(superclass, targetFqn, visited);
}
return false;
}
}

View File

@@ -0,0 +1,170 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class CfgBuilder {
public static ControlFlowGraph build(MethodDeclaration method) {
ControlFlowGraph cfg = new ControlFlowGraph(method);
if (method.getBody() == null) {
cfg.getEntryNode().addSuccessor(cfg.getExitNode());
return cfg;
}
// Build CFG recursively
List<ControlFlowGraph.CfgNode> exits = buildFlow(method.getBody(), List.of(cfg.getEntryNode()), cfg);
for (ControlFlowGraph.CfgNode exit : exits) {
exit.addSuccessor(cfg.getExitNode());
}
return cfg;
}
private static List<ControlFlowGraph.CfgNode> buildFlow(
ASTNode node,
List<ControlFlowGraph.CfgNode> sources,
ControlFlowGraph cfg) {
if (node == null || sources.isEmpty()) {
return sources;
}
if (node instanceof Block block) {
List<ControlFlowGraph.CfgNode> currentSources = sources;
for (Object stmtObj : block.statements()) {
currentSources = buildFlow((ASTNode) stmtObj, currentSources, cfg);
}
return currentSources;
}
if (node instanceof IfStatement ifStmt) {
// Condition node
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(ifStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(condNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(condNode);
}
List<ControlFlowGraph.CfgNode> thenExits = buildFlow(ifStmt.getThenStatement(), List.of(condNode), cfg);
List<ControlFlowGraph.CfgNode> elseExits;
if (ifStmt.getElseStatement() != null) {
elseExits = buildFlow(ifStmt.getElseStatement(), List.of(condNode), cfg);
} else {
elseExits = List.of(condNode);
}
List<ControlFlowGraph.CfgNode> combinedExits = new ArrayList<>();
combinedExits.addAll(thenExits);
combinedExits.addAll(elseExits);
return combinedExits;
}
if (node instanceof WhileStatement whileStmt) {
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(whileStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(condNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(condNode);
}
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(whileStmt.getBody(), List.of(condNode), cfg);
for (ControlFlowGraph.CfgNode exit : bodyExits) {
exit.addSuccessor(condNode); // Loop back
}
return List.of(condNode); // Exit loop when cond is false
}
if (node instanceof DoStatement doStmt) {
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(condNode);
List<ControlFlowGraph.CfgNode> bodySources = new ArrayList<>(sources);
bodySources.add(condNode);
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(doStmt.getBody(), bodySources, cfg);
for (ControlFlowGraph.CfgNode exit : bodyExits) {
exit.addSuccessor(condNode);
}
return List.of(condNode);
}
if (node instanceof ForStatement forStmt) {
// Initializers
List<ControlFlowGraph.CfgNode> currentSources = sources;
for (Object initObj : forStmt.initializers()) {
ControlFlowGraph.CfgNode initNode = new ControlFlowGraph.CfgNode((ASTNode) initObj, ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(initNode);
for (ControlFlowGraph.CfgNode src : currentSources) {
src.addSuccessor(initNode);
}
currentSources = List.of(initNode);
}
// Condition
ControlFlowGraph.CfgNode condNode;
if (forStmt.getExpression() != null) {
condNode = new ControlFlowGraph.CfgNode(forStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
} else {
condNode = new ControlFlowGraph.CfgNode(forStmt, ControlFlowGraph.CfgNodeType.STATEMENT); // Dummy node
}
cfg.addNode(condNode);
for (ControlFlowGraph.CfgNode src : currentSources) {
src.addSuccessor(condNode);
}
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(forStmt.getBody(), List.of(condNode), cfg);
// Updaters
List<ControlFlowGraph.CfgNode> updaterSources = bodyExits;
for (Object updateObj : forStmt.updaters()) {
ControlFlowGraph.CfgNode updateNode = new ControlFlowGraph.CfgNode((ASTNode) updateObj, ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(updateNode);
for (ControlFlowGraph.CfgNode src : updaterSources) {
src.addSuccessor(updateNode);
}
updaterSources = List.of(updateNode);
}
for (ControlFlowGraph.CfgNode src : updaterSources) {
src.addSuccessor(condNode); // Loop back
}
return List.of(condNode);
}
if (node instanceof EnhancedForStatement effStmt) {
ControlFlowGraph.CfgNode loopNode = new ControlFlowGraph.CfgNode(effStmt.getParameter(), ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(loopNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(loopNode);
}
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(effStmt.getBody(), List.of(loopNode), cfg);
for (ControlFlowGraph.CfgNode exit : bodyExits) {
exit.addSuccessor(loopNode); // Loop back
}
return List.of(loopNode);
}
if (node instanceof ReturnStatement returnStmt) {
ControlFlowGraph.CfgNode retNode = new ControlFlowGraph.CfgNode(returnStmt, ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(retNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(retNode);
}
// Return flows to exit, not to subsequent sequential nodes
retNode.addSuccessor(cfg.getExitNode());
return Collections.emptyList();
}
// Default: general statement
ControlFlowGraph.CfgNode stmtNode = new ControlFlowGraph.CfgNode(node, ControlFlowGraph.CfgNodeType.STATEMENT);
cfg.addNode(stmtNode);
for (ControlFlowGraph.CfgNode src : sources) {
src.addSuccessor(stmtNode);
}
return List.of(stmtNode);
}
}

View File

@@ -30,7 +30,12 @@ public class CodebaseContext {
private final Map<String, String> simpleNameToFqn = new HashMap<>();
private final Set<String> ambiguousSimpleNames = new HashSet<>();
private final ConstantResolver constantResolver = new ConstantResolver();
private final StateResolver stateResolver = new StateResolver(this);
private final PropertyResolver propertyResolver = new PropertyResolver();
public ConstantResolver getConstantResolver() {
return constantResolver;
}
private final List<String> activeProfiles = new ArrayList<>();
private Map<String, Map<String, String>> allProperties = new HashMap<>();
private List<LibraryHint> libraryHints = new ArrayList<>();
@@ -65,7 +70,7 @@ public class CodebaseContext {
return Collections.unmodifiableList(libraryHints);
}
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/**", "**/node_modules/**", "**/out/**"));
private final Set<String> ignorePatterns = new HashSet<>(Set.of("**/test/**", "**/build/classes/**", "**/build/libs/**", "**/build/tmp/**", "**/build/reports/**", "**/build/test-results/**", "**/node_modules/**", "**/out/**"));
private String[] classpath = new String[0];
private String[] sourcepath = new String[0];
private boolean resolveBindings = false;
@@ -135,7 +140,7 @@ public class CodebaseContext {
if (imp.isOnDemand()) {
// Star import: import static com.example.C.*;
TypeDeclaration td = getTypeDeclaration(impName, contextCu);
if (td != null && hasField(td, memberName)) {
if (td != null && (hasField(td, memberName) || hasMethod(td, memberName))) {
return td;
}
} else {
@@ -150,6 +155,15 @@ public class CodebaseContext {
return null;
}
public boolean hasMethod(TypeDeclaration td, String name) {
for (MethodDeclaration md : td.getMethods()) {
if (md.getName().getIdentifier().equals(name)) {
return true;
}
}
return false;
}
public boolean hasField(TypeDeclaration td, String name) {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
@@ -191,6 +205,8 @@ public class CodebaseContext {
indexType(td, packageName, javaFile);
} else if (type instanceof EnumDeclaration ed) {
indexEnum(ed, packageName, javaFile);
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration rd) {
indexRecord(rd, packageName, javaFile);
}
}
}
@@ -228,9 +244,42 @@ public class CodebaseContext {
}
// Recursively index nested types
for (Object type : td.getTypes()) {
if (type instanceof TypeDeclaration nestedTd) {
for (Object decl : td.bodyDeclarations()) {
if (decl instanceof TypeDeclaration nestedTd) {
indexType(nestedTd, fqn, javaFile);
} else if (decl instanceof EnumDeclaration nestedEd) {
indexEnum(nestedEd, fqn, javaFile);
} else if (decl instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
indexRecord(nestedRd, fqn, javaFile);
}
}
}
private void indexRecord(org.eclipse.jdt.core.dom.RecordDeclaration rd, String parentFqn, Path javaFile) {
String simpleName = rd.getName().getIdentifier();
String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName;
// Treat as a class since it is a type
classes.put(fqn, (CompilationUnit) rd.getRoot());
classPaths.put(fqn, javaFile);
if (simpleNameToFqn.containsKey(simpleName)) {
String existingFqn = simpleNameToFqn.get(simpleName);
if (!existingFqn.equals(fqn)) {
ambiguousSimpleNames.add(simpleName);
}
} else {
simpleNameToFqn.put(simpleName, fqn);
}
// Recursively index nested types
for (Object decl : rd.bodyDeclarations()) {
if (decl instanceof TypeDeclaration nestedTd) {
indexType(nestedTd, fqn, javaFile);
} else if (decl instanceof EnumDeclaration nestedEd) {
indexEnum(nestedEd, fqn, javaFile);
} else if (decl instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
indexRecord(nestedRd, fqn, javaFile);
}
}
}
@@ -238,6 +287,7 @@ public class CodebaseContext {
public List<String> getImplementations(String interfaceName) {
Set<String> allImpls = new HashSet<>();
collectImplementations(interfaceName, allImpls, new HashSet<>());
System.out.println("GET IMPLEMENTATIONS FOR: " + interfaceName + " -> " + allImpls);
return new ArrayList<>(allImpls);
}
@@ -287,6 +337,21 @@ public class CodebaseContext {
if (!simpleNameToFqn.containsKey(simpleName)) {
simpleNameToFqn.put(simpleName, fqn);
}
// Recursively index nested types inside enum body
for (Object decl : ed.bodyDeclarations()) {
if (decl instanceof TypeDeclaration nestedTd) {
indexType(nestedTd, fqn, javaFile);
} else if (decl instanceof EnumDeclaration nestedEd) {
indexEnum(nestedEd, fqn, javaFile);
} else if (decl instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
indexRecord(nestedRd, fqn, javaFile);
}
}
}
public Map<String, List<String>> getEnumValuesMap() {
return enumValues;
}
public List<String> getEnumValues(String fqnOrSimpleName) {
@@ -300,110 +365,7 @@ public class CodebaseContext {
}
public State resolveState(Expression expr, CompilationUnit cu) {
if (expr == null)
return null;
// 1. Check for constants
String resolvedValue = constantResolver.resolve(expr, this);
if (resolvedValue != null) {
return State.of(expr.toString(), resolvedValue);
}
// 2. Check for @Value fields (SimpleName)
if (expr instanceof SimpleName sn) {
String fieldName = sn.getIdentifier();
TypeDeclaration td = findEnclosingType(sn);
if (td != null) {
String value = findValueFromField(td, fieldName);
if (value != null) {
return State.of(fieldName, value);
}
}
}
if (expr instanceof StringLiteral sl)
return State.of(sl.getLiteralValue());
String raw = expr.toString();
if (expr instanceof QualifiedName qn) {
String full = resolveQualifiedName(qn, cu);
return State.of(raw, full);
}
if (expr instanceof SimpleName sn) {
String full = resolveSimpleName(sn, cu);
return State.of(raw, full);
}
return State.of(raw);
}
private String findValueFromField(TypeDeclaration td, String fieldName) {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(fieldName)) {
// Check for @Value annotation
for (Object mod : fd.modifiers()) {
if (mod instanceof Annotation ann) {
String name = ann.getTypeName().getFullyQualifiedName();
if (name.endsWith("Value")) {
return extractAnnotationValue(ann);
}
}
}
}
}
}
}
return null;
}
private String extractAnnotationValue(Annotation ann) {
if (ann instanceof SingleMemberAnnotation sma) {
return stripQuotes(sma.getValue().toString());
} else if (ann instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if ("value".equals(pair.getName().getIdentifier())) {
return stripQuotes(pair.getValue().toString());
}
}
}
return null;
}
private String stripQuotes(String s) {
if (s == null) return null;
return s.replaceAll("^\"|\"$", "");
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
String qualifier = qn.getQualifier().toString();
String name = qn.getName().getIdentifier();
TypeDeclaration td = getTypeDeclaration(qualifier, cu);
if (td != null)
return getFqn(td) + "." + name;
return qn.getFullyQualifiedName();
}
private String resolveSimpleName(SimpleName sn, CompilationUnit cu) {
String name = sn.getIdentifier();
if (cu == null)
return name;
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + name))
return impName;
}
return name;
return stateResolver.resolveState(expr, cu);
}
private CompilationUnit parse(String source, String unitName) {
@@ -411,6 +373,7 @@ public class CodebaseContext {
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(resolveBindings);
parser.setBindingsRecovery(true);
Map<String, String> options = JavaCore.getOptions();
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
@@ -456,6 +419,14 @@ public class CodebaseContext {
// 2. Check imports in contextCu
if (contextCu != null) {
for (Object typeObj : contextCu.types()) {
if (typeObj instanceof TypeDeclaration tdDecl) {
if (tdDecl.getName().getIdentifier().equals(name)) {
return tdDecl;
}
}
}
for (Object impObj : contextCu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
@@ -492,11 +463,20 @@ public class CodebaseContext {
}
public String getSuperclassFqn(TypeDeclaration td) {
if (td == null) return null;
ITypeBinding binding = td.resolveBinding();
if (binding != null) {
ITypeBinding superBinding = binding.getSuperclass();
if (superBinding != null) {
return superBinding.getErasure().getQualifiedName();
}
}
Type superType = td.getSuperclassType();
if (superType == null) return null;
String superName = extractTypeName(superType);
CompilationUnit cu = (CompilationUnit) td.getRoot();
CompilationUnit cu = (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null;
TypeDeclaration superTd = getTypeDeclaration(superName, cu);
return superTd != null ? getFqn(superTd) : superName;
}
@@ -513,6 +493,56 @@ public class CodebaseContext {
}
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
if (td == null) return null;
ITypeBinding binding = td.resolveBinding();
if (binding != null && includeSuper) {
IMethodBinding methodBinding = findMethodBinding(binding, methodName);
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
CompilationUnit declaringCu = classes.get(methodBinding.getDeclaringClass().getErasure().getQualifiedName());
if (declaringCu != null) {
ASTNode declNode = declaringCu.findDeclaringNode(methodBinding.getKey());
if (declNode instanceof MethodDeclaration) {
return (MethodDeclaration) declNode;
}
}
}
}
return findMethodDeclarationLegacy(td, methodName, includeSuper);
}
private IMethodBinding findMethodBinding(ITypeBinding typeBinding, String methodName) {
return findMethodBinding(typeBinding, methodName, new java.util.HashSet<>());
}
private IMethodBinding findMethodBinding(ITypeBinding typeBinding, String methodName, Set<String> visited) {
if (typeBinding == null) return null;
String key = typeBinding.getKey();
if (key != null && !visited.add(key)) {
return null;
}
for (IMethodBinding mb : typeBinding.getDeclaredMethods()) {
if (mb.getName().equals(methodName)) {
return mb;
}
}
ITypeBinding superclass = typeBinding.getSuperclass();
if (superclass != null) {
IMethodBinding mb = findMethodBinding(superclass, methodName, visited);
if (mb != null) return mb;
}
for (ITypeBinding intf : typeBinding.getInterfaces()) {
IMethodBinding mb = findMethodBinding(intf, methodName, visited);
if (mb != null) return mb;
}
return null;
}
private MethodDeclaration findMethodDeclarationLegacy(TypeDeclaration td, String methodName, boolean includeSuper) {
if (td == null) return null;
for (MethodDeclaration method : td.getMethods()) {
if (method.getName().getIdentifier().equals(methodName)) {
return method;
@@ -524,7 +554,7 @@ public class CodebaseContext {
if (superFqn != null) {
TypeDeclaration superTd = getTypeDeclaration(superFqn);
if (superTd != null) {
return findMethodDeclaration(superTd, methodName, true);
return findMethodDeclarationLegacy(superTd, methodName, true);
}
}
@@ -532,9 +562,9 @@ public class CodebaseContext {
for (Object interfaceTypeObj : td.superInterfaceTypes()) {
Type interfaceType = (Type) interfaceTypeObj;
String interfaceName = extractTypeName(interfaceType);
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (CompilationUnit) td.getRoot());
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null);
if (interfaceTd != null) {
MethodDeclaration found = findMethodDeclaration(interfaceTd, methodName, true);
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true);
if (found != null) return found;
}
}
@@ -542,15 +572,107 @@ public class CodebaseContext {
return null;
}
public String getFqn(TypeDeclaration td) {
StringBuilder sb = new StringBuilder(td.getName().getIdentifier());
ASTNode current = td.getParent();
while (current instanceof TypeDeclaration parent) {
public AbstractTypeDeclaration getAbstractTypeDeclaration(String name) {
// Try exact FQN match first
CompilationUnit cu = classes.get(name);
if (cu != null)
return findAbstractTypeInCu(cu, name);
// If it's a simple name, check if it's ambiguous
if (ambiguousSimpleNames.contains(name)) {
return null;
}
// If not ambiguous, return the one we found during scan
String fqn = simpleNameToFqn.get(name);
if (fqn != null) {
cu = classes.get(fqn);
if (cu != null)
return findAbstractTypeInCu(cu, fqn);
}
return null;
}
public AbstractTypeDeclaration getAbstractTypeDeclaration(String name, CompilationUnit contextCu) {
if (name == null || name.isEmpty())
return null;
// 1. Check if it's already an FQN
if (classes.containsKey(name))
return getAbstractTypeDeclaration(name);
// 2. Check imports in contextCu
if (contextCu != null) {
for (Object impObj : contextCu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (!imp.isStatic() && !imp.isOnDemand()) {
if (impName.endsWith("." + name))
return getAbstractTypeDeclaration(impName);
}
}
// 3. Check same package
String packageName = contextCu.getPackage() != null ? contextCu.getPackage().getName().getFullyQualifiedName() : "";
String localFqn = packageName.isEmpty() ? name : packageName + "." + name;
if (classes.containsKey(localFqn))
return getAbstractTypeDeclaration(localFqn);
// 4. Check on-demand imports (star imports)
for (Object impObj : contextCu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
if (imp.isOnDemand()) {
String starFqn = imp.getName().getFullyQualifiedName() + "." + name;
if (classes.containsKey(starFqn))
return getAbstractTypeDeclaration(starFqn);
}
}
// 5. Fallback to java.lang (common)
String langFqn = "java.lang." + name;
if (classes.containsKey(langFqn))
return getAbstractTypeDeclaration(langFqn);
}
// 6. Last resort: global simple name match
return getAbstractTypeDeclaration(name);
}
private AbstractTypeDeclaration findAbstractTypeInCu(CompilationUnit cu, String fqn) {
for (Object type : cu.types()) {
if (type instanceof AbstractTypeDeclaration atd) {
AbstractTypeDeclaration found = findNestedAbstractType(atd, fqn);
if (found != null) return found;
}
}
return null;
}
private AbstractTypeDeclaration findNestedAbstractType(AbstractTypeDeclaration atd, String targetFqn) {
if (getFqn(atd).equals(targetFqn)) return atd;
if (atd instanceof TypeDeclaration td) {
for (TypeDeclaration nested : td.getTypes()) {
AbstractTypeDeclaration found = findNestedAbstractType(nested, targetFqn);
if (found != null) return found;
}
}
return null;
}
public String getFqn(AbstractTypeDeclaration atd) {
if (atd == null) return null;
StringBuilder sb = new StringBuilder(atd.getName().getIdentifier());
ASTNode current = atd.getParent();
while (current instanceof AbstractTypeDeclaration parent) {
sb.insert(0, parent.getName().getIdentifier() + ".");
current = parent.getParent();
}
CompilationUnit cu = (CompilationUnit) td.getRoot();
CompilationUnit cu = (atd.getRoot() instanceof CompilationUnit) ? (CompilationUnit) atd.getRoot() : null;
if (cu == null) {
return sb.toString();
}
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
String fqn = sb.toString();
return packageName.isEmpty() ? fqn : packageName + "." + fqn;
@@ -624,10 +746,19 @@ public class CodebaseContext {
}
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
ITypeBinding binding = td.resolveBinding();
if (binding != null) {
boolean isSubtype = BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.builders.StateMachineConfigurer")
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.StateMachineConfigurerAdapter")
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter");
if (isSubtype) {
return true;
}
}
return extendsStateMachineConfigurerAdapterLegacy(td, new HashSet<>());
}
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td, Set<String> visited) {
private boolean extendsStateMachineConfigurerAdapterLegacy(TypeDeclaration td, Set<String> visited) {
String fqn = getFqn(td);
if (visited.contains(fqn)) {
return false;
@@ -644,7 +775,7 @@ public class CodebaseContext {
if (knownAdapters.contains(superclassName))
return true;
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd, visited))
if (superTd != null && extendsStateMachineConfigurerAdapterLegacy(superTd, visited))
return true;
}
@@ -655,7 +786,7 @@ public class CodebaseContext {
if (knownAdapters.contains(interfaceName))
return true;
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd, visited))
if (interfaceTd != null && extendsStateMachineConfigurerAdapterLegacy(interfaceTd, visited))
return true;
}
}

View File

@@ -0,0 +1,78 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class ControlFlowGraph {
private final MethodDeclaration method;
private final CfgNode entryNode = new CfgNode(null, CfgNodeType.ENTRY);
private final CfgNode exitNode = new CfgNode(null, CfgNodeType.EXIT);
private final List<CfgNode> nodes = new ArrayList<>();
public enum CfgNodeType {
ENTRY,
EXIT,
STATEMENT
}
public static class CfgNode {
private final ASTNode astNode;
private final CfgNodeType type;
private final Set<CfgNode> predecessors = new HashSet<>();
private final Set<CfgNode> successors = new HashSet<>();
public CfgNode(ASTNode astNode, CfgNodeType type) {
this.astNode = astNode;
this.type = type;
}
public ASTNode getAstNode() {
return astNode;
}
public CfgNodeType getType() {
return type;
}
public Set<CfgNode> getPredecessors() {
return predecessors;
}
public Set<CfgNode> getSuccessors() {
return successors;
}
public void addSuccessor(CfgNode succ) {
if (succ != null) {
this.successors.add(succ);
succ.predecessors.add(this);
}
}
}
public ControlFlowGraph(MethodDeclaration method) {
this.method = method;
this.nodes.add(entryNode);
this.nodes.add(exitNode);
}
public MethodDeclaration getMethod() {
return method;
}
public CfgNode getEntryNode() {
return entryNode;
}
public CfgNode getExitNode() {
return exitNode;
}
public List<CfgNode> getNodes() {
return nodes;
}
public void addNode(CfgNode node) {
this.nodes.add(node);
}
}

View File

@@ -0,0 +1,18 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.Expression;
import java.util.List;
public interface DataFlowModel {
/**
* Resolves all possible Expressions that define the value of the given expression
* at its location in the control flow.
*/
List<Expression> getReachingDefinitions(Expression expr);
/**
* Resolves the single constant/literal value if all definitions converge,
* or if a single value is expected.
*/
String resolveValue(Expression expr, CodebaseContext context);
}

View File

@@ -0,0 +1,892 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class JdtDataFlowModel implements DataFlowModel {
private final CodebaseContext context;
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
private final Map<ASTNode, Map<IVariableBinding, Expression>> objectFieldBindings = new HashMap<>();
public JdtDataFlowModel(CodebaseContext context) {
this.context = context;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof MethodDeclaration) {
return (MethodDeclaration) current;
}
current = current.getParent();
}
return null;
}
private ReachingDefinitions getReachingDefinitionsForMethod(MethodDeclaration method) {
if (method == null) return null;
return rdCache.computeIfAbsent(method, m -> {
ControlFlowGraph cfg = cfgCache.computeIfAbsent(m, CfgBuilder::build);
return new ReachingDefinitions(cfg);
});
}
@Override
public List<Expression> getReachingDefinitions(Expression expr) {
return getReachingDefinitions(expr, new HashSet<>(), new HashMap<>(), new HashMap<>(), 0);
}
private List<Expression> getReachingDefinitions(
Expression expr,
Set<ASTNode> visited,
Map<IVariableBinding, Expression> paramBindings,
Map<IVariableBinding, Expression> instanceFieldBindings,
int depth) {
if (expr == null || depth > 50 || !visited.add(expr)) {
return List.of();
}
// 1. Handle Ternary / Conditional Expression
if (expr instanceof ConditionalExpression ce) {
String condVal = resolveValue(ce.getExpression(), context);
if ("true".equals(condVal)) {
List<Expression> resolved = getReachingDefinitions(ce.getThenExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
visited.remove(expr);
return resolved;
} else if ("false".equals(condVal)) {
List<Expression> resolved = getReachingDefinitions(ce.getElseExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
visited.remove(expr);
return resolved;
} else {
visited.remove(expr);
return List.of(ce);
}
}
// 2. Handle Field constant / field initializer propagation
IVariableBinding vb = getVariableBinding(expr);
if (vb != null && vb.isField()) {
if (instanceFieldBindings.containsKey(vb)) {
Expression fieldVal = instanceFieldBindings.get(vb);
List<Expression> resolved = getReachingDefinitions(fieldVal, visited, paramBindings, instanceFieldBindings, depth + 1);
visited.remove(expr);
return resolved;
}
Expression fieldInit = findFieldInitializer(vb, expr);
if (fieldInit != null) {
List<Expression> resolved = getReachingDefinitions(fieldInit, visited, paramBindings, instanceFieldBindings, depth + 1);
visited.remove(expr);
return resolved;
}
}
// 3. Handle Parameter mapping for SimpleName
if (expr instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding paramVb && paramBindings.containsKey(paramVb)) {
Expression argExpr = paramBindings.get(paramVb);
List<Expression> resolved = getReachingDefinitions(argExpr, visited, paramBindings, instanceFieldBindings, depth + 1);
visited.remove(expr);
return resolved;
}
// Fallback: Local Reaching Definitions
MethodDeclaration md = findEnclosingMethod(sn);
ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
if (rd != null) {
Set<ASTNode> defs = rd.getReachingDefinitions(sn, sn.getIdentifier());
if (!defs.isEmpty()) {
List<Expression> exprs = new ArrayList<>();
for (ASTNode def : defs) {
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
if (defExpr != null) {
exprs.addAll(getReachingDefinitions(defExpr, visited, paramBindings, instanceFieldBindings, depth + 1));
} else {
exprs.add(expr);
}
}
visited.remove(expr);
return exprs;
}
}
}
// Handle ClassInstanceCreation
if (expr instanceof ClassInstanceCreation cic) {
getOrCreateFieldBindings(cic, paramBindings, instanceFieldBindings);
visited.remove(expr);
return List.of(cic);
}
// Handle CastExpression
if (expr instanceof CastExpression ce) {
List<Expression> resolved = getReachingDefinitions(ce.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
visited.remove(expr);
return resolved;
}
// Handle ParenthesizedExpression
if (expr instanceof ParenthesizedExpression pe) {
List<Expression> resolved = getReachingDefinitions(pe.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
visited.remove(expr);
return resolved;
}
// 4. Handle Method Invocations (Static Factory methods / Transforming Getters)
if (expr instanceof MethodInvocation mi) {
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, instanceFieldBindings, depth);
if (!targets.isEmpty()) {
List<Expression> results = new ArrayList<>();
for (TargetMethod target : targets) {
MethodDeclaration md = target.declaration;
if (md != null && md.getBody() != null) {
Map<IVariableBinding, Expression> newParamBindings = new HashMap<>(paramBindings);
List<?> parameters = md.parameters();
List<?> arguments = mi.arguments();
int count = Math.min(parameters.size(), arguments.size());
for (int i = 0; i < count; i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i);
IVariableBinding paramVb = svd.resolveBinding();
if (paramVb != null) {
newParamBindings.put(paramVb, (Expression) arguments.get(i));
}
}
// Evaluate mutator/setter method body
evaluateMethodMutations(md, mi.arguments(), target.fieldBindings);
List<ReturnStatement> returns = findReturnStatements(md.getBody());
if (!returns.isEmpty()) {
for (ReturnStatement rs : returns) {
if (rs.getExpression() != null) {
Expression retExpr = rs.getExpression();
Expression unwrapped = retExpr;
while (unwrapped instanceof ParenthesizedExpression pe) {
unwrapped = pe.getExpression();
}
while (unwrapped instanceof CastExpression ce) {
unwrapped = ce.getExpression();
}
if (unwrapped instanceof ThisExpression) {
if (mi.getExpression() != null) {
results.addAll(getReachingDefinitions(mi.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1));
} else {
results.add(unwrapped);
}
} else {
results.addAll(getReachingDefinitions(retExpr, visited, newParamBindings, target.fieldBindings, depth + 1));
}
}
}
}
}
}
if (!results.isEmpty()) {
visited.remove(expr);
return results;
}
}
}
}
// Handle Super Method Invocations
if (expr instanceof SuperMethodInvocation smi) {
IMethodBinding mb = smi.resolveMethodBinding();
if (mb != null) {
MethodDeclaration md = findMethodDeclaration(mb);
if (md != null && md.getBody() != null) {
Map<IVariableBinding, Expression> newParamBindings = new HashMap<>(paramBindings);
List<?> parameters = md.parameters();
List<?> arguments = smi.arguments();
int count = Math.min(parameters.size(), arguments.size());
for (int i = 0; i < count; i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i);
IVariableBinding paramVb = svd.resolveBinding();
if (paramVb != null) {
newParamBindings.put(paramVb, (Expression) arguments.get(i));
}
}
List<ReturnStatement> returns = findReturnStatements(md.getBody());
if (!returns.isEmpty()) {
List<Expression> results = new ArrayList<>();
for (ReturnStatement rs : returns) {
if (rs.getExpression() != null) {
results.addAll(getReachingDefinitions(rs.getExpression(), visited, newParamBindings, instanceFieldBindings, depth + 1));
}
}
visited.remove(expr);
return results;
}
}
}
}
visited.remove(expr);
return List.of(expr);
}
private IVariableBinding getVariableBinding(Expression expr) {
if (expr instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding varBinding) return varBinding;
} else if (expr instanceof QualifiedName qn) {
IBinding b = qn.resolveBinding();
if (b instanceof IVariableBinding varBinding) return varBinding;
} else if (expr instanceof FieldAccess fa) {
return fa.resolveFieldBinding();
} else if (expr instanceof SuperFieldAccess sfa) {
return sfa.resolveFieldBinding();
}
return null;
}
private Expression findFieldInitializer(IVariableBinding vb, ASTNode contextNode) {
if (vb == null) return null;
ITypeBinding declaringClass = vb.getDeclaringClass();
if (declaringClass == null) return null;
String classFqn = declaringClass.getErasure().getQualifiedName();
if (classFqn == null || classFqn.isEmpty()) return null;
TypeDeclaration td = context.getTypeDeclaration(classFqn);
if (td == null) {
CompilationUnit cu = null;
ASTNode root = contextNode.getRoot();
if (root instanceof CompilationUnit) {
cu = (CompilationUnit) root;
}
td = context.getTypeDeclaration(classFqn, cu);
}
if (td != null) {
// 1. Try field declaration initializer
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(vb.getName())) {
if (fragment.getInitializer() != null) {
return fragment.getInitializer();
}
}
}
}
}
// 2. Try constructor assignments
for (MethodDeclaration md : td.getMethods()) {
if (md.isConstructor() && md.getBody() != null) {
final Expression[] assignedExpr = new Expression[1];
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
boolean matches = false;
if (lhs instanceof SimpleName snLhs && snLhs.getIdentifier().equals(vb.getName())) {
IBinding b = snLhs.resolveBinding();
if (b instanceof IVariableBinding resolvedVb && resolvedVb.getKey().equals(vb.getKey())) {
matches = true;
}
} else if (lhs instanceof FieldAccess fa && fa.getName().getIdentifier().equals(vb.getName()) && fa.getExpression() instanceof ThisExpression) {
IBinding b = fa.getName().resolveBinding();
if (b instanceof IVariableBinding resolvedVb && resolvedVb.getKey().equals(vb.getKey())) {
matches = true;
}
}
if (matches) {
assignedExpr[0] = assignment.getRightHandSide();
}
return super.visit(assignment);
}
});
if (assignedExpr[0] != null) {
return assignedExpr[0];
}
}
}
}
return null;
}
private Map<IVariableBinding, Expression> getOrCreateFieldBindings(
ClassInstanceCreation cic,
Map<IVariableBinding, Expression> paramBindings,
Map<IVariableBinding, Expression> instanceFieldBindings) {
Map<IVariableBinding, Expression> bindings = objectFieldBindings.get(cic);
if (bindings == null) {
bindings = buildFieldBindingsFromCic(cic, paramBindings, instanceFieldBindings);
objectFieldBindings.put(cic, bindings);
}
return bindings;
}
private Map<IVariableBinding, Expression> getOrCreateFieldBindings(ClassInstanceCreation cic) {
Map<IVariableBinding, Expression> bindings = objectFieldBindings.get(cic);
if (bindings == null) {
bindings = buildFieldBindingsFromCic(cic);
objectFieldBindings.put(cic, bindings);
}
return bindings;
}
private Map<IVariableBinding, Expression> buildFieldBindingsFromCic(ClassInstanceCreation cic) {
return buildFieldBindingsFromCic(cic, new HashMap<>(), new HashMap<>());
}
private Map<IVariableBinding, Expression> buildFieldBindingsFromCic(
ClassInstanceCreation cic,
Map<IVariableBinding, Expression> callerParamBindings,
Map<IVariableBinding, Expression> callerInstanceFieldBindings) {
Map<IVariableBinding, Expression> fieldBindings = new HashMap<>();
IMethodBinding cb = cic.resolveConstructorBinding();
if (cb == null) return fieldBindings;
List<Expression> resolvedArguments = new ArrayList<>();
for (Object argObj : cic.arguments()) {
Expression arg = (Expression) argObj;
List<Expression> resolved = getReachingDefinitions(arg, new HashSet<>(), callerParamBindings, callerInstanceFieldBindings, 0);
if (!resolved.isEmpty()) {
resolvedArguments.add(resolved.get(0));
} else {
resolvedArguments.add(arg);
}
}
evaluateConstructor(cb, resolvedArguments, new HashMap<>(), fieldBindings, 0);
return fieldBindings;
}
private void evaluateConstructor(
IMethodBinding cb,
List<?> arguments,
Map<IVariableBinding, Expression> callerParamValues,
Map<IVariableBinding, Expression> fieldBindings,
int depth) {
if (cb == null || depth > 10) return;
MethodDeclaration cd = findMethodDeclaration(cb);
if (cd == null || cd.getBody() == null) return;
Map<IVariableBinding, Expression> currentParamValues = new HashMap<>();
List<?> parameters = cd.parameters();
int count = Math.min(parameters.size(), arguments.size());
for (int i = 0; i < count; i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i);
IVariableBinding paramVb = svd.resolveBinding();
if (paramVb != null) {
Expression arg = (Expression) arguments.get(i);
Expression resolvedArg = resolveParamValue(arg, callerParamValues);
currentParamValues.put(paramVb, resolvedArg);
}
}
List<?> statements = cd.getBody().statements();
if (!statements.isEmpty()) {
Object firstStmt = statements.get(0);
if (firstStmt instanceof ConstructorInvocation ci) {
IMethodBinding targetCb = ci.resolveConstructorBinding();
if (targetCb != null) {
evaluateConstructor(targetCb, ci.arguments(), currentParamValues, fieldBindings, depth + 1);
}
} else if (firstStmt instanceof SuperConstructorInvocation sci) {
IMethodBinding targetCb = sci.resolveConstructorBinding();
if (targetCb != null) {
evaluateConstructor(targetCb, sci.arguments(), currentParamValues, fieldBindings, depth + 1);
}
}
}
cd.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
Expression rhs = assignment.getRightHandSide();
IVariableBinding fieldVb = null;
if (lhs instanceof FieldAccess fa) {
if (fa.getExpression() instanceof ThisExpression) {
IBinding b = fa.getName().resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
} else if (lhs instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
if (fieldVb != null) {
Expression resolvedRhs = resolveParamValue(rhs, currentParamValues);
fieldBindings.put(fieldVb, resolvedRhs);
}
return super.visit(assignment);
}
});
}
private Expression resolveParamValue(Expression expr, Map<IVariableBinding, Expression> paramValues) {
if (expr instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding vb && paramValues.containsKey(vb)) {
return paramValues.get(vb);
}
} else if (expr instanceof CastExpression ce) {
Expression resolved = resolveParamValue(ce.getExpression(), paramValues);
if (resolved != ce.getExpression()) {
return resolved;
}
} else if (expr instanceof ParenthesizedExpression pe) {
Expression resolved = resolveParamValue(pe.getExpression(), paramValues);
if (resolved != pe.getExpression()) {
return resolved;
}
}
return expr;
}
private static class TargetMethod {
final MethodDeclaration declaration;
final Map<IVariableBinding, Expression> fieldBindings;
TargetMethod(MethodDeclaration declaration, Map<IVariableBinding, Expression> fieldBindings) {
this.declaration = declaration;
this.fieldBindings = fieldBindings;
}
}
private List<TargetMethod> resolveTargets(
MethodInvocation mi,
IMethodBinding mb,
Set<ASTNode> visited,
Map<IVariableBinding, Expression> paramBindings,
Map<IVariableBinding, Expression> instanceFieldBindings,
int depth) {
Expression receiver = mi.getExpression();
boolean isThisReceiver = (receiver == null || receiver instanceof ThisExpression);
List<TargetMethod> targets = new ArrayList<>();
if (receiver != null) {
// Find concrete ClassInstanceCreation definitions in the reaching definitions path of the receiver
List<Expression> receiverDefs = getReachingDefinitions(receiver, visited, paramBindings, instanceFieldBindings, depth + 1);
List<ClassInstanceCreation> concreteCics = new ArrayList<>();
for (Expression rDef : receiverDefs) {
if (rDef instanceof ClassInstanceCreation cic) {
concreteCics.add(cic);
}
}
if (!concreteCics.isEmpty()) {
// Type narrowing is active! Only trace through the concrete instantiated types.
for (ClassInstanceCreation cic : concreteCics) {
ITypeBinding concreteType = null;
if (cic.resolveConstructorBinding() != null) {
concreteType = cic.resolveConstructorBinding().getDeclaringClass();
}
if (concreteType == null) {
concreteType = cic.resolveTypeBinding();
}
if (concreteType != null) {
MethodDeclaration md = findMethodDeclarationInType(concreteType, mb);
if (md != null) {
Map<IVariableBinding, Expression> concreteFieldBindings =
getOrCreateFieldBindings(cic);
// Apply flow-sensitive intermediate local variable mutations
if (receiver instanceof SimpleName sn) {
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
if (enclosingMethod != null) {
ReachingDefinitions rd = getReachingDefinitionsForMethod(enclosingMethod);
if (rd != null) {
Set<ASTNode> defs = rd.getReachingDefinitions(sn, sn.getIdentifier());
for (ASTNode def : defs) {
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
if (defExpr == cic || isExpressionWrapping(defExpr, cic)) {
applyIntermediateMutations(sn, def, mi, enclosingMethod, concreteFieldBindings, paramBindings, visited, depth);
}
}
}
}
}
targets.add(new TargetMethod(md, concreteFieldBindings));
}
}
}
return targets;
}
}
// Fallback: Polymorphic target discovery (including interface implementations)
ITypeBinding declaringClass = mb.getDeclaringClass();
if (declaringClass != null) {
String declaringClassFqn = declaringClass.getErasure().getQualifiedName();
if (declaringClassFqn != null && !declaringClassFqn.isEmpty()) {
// Find all known implementation subclasses in the codebase
List<String> implClassFqns = context.getImplementations(declaringClassFqn);
if (implClassFqns != null && !implClassFqns.isEmpty()) {
for (String implFqn : implClassFqns) {
TypeDeclaration td = context.getTypeDeclaration(implFqn);
if (td != null) {
MethodDeclaration md = findMethodDeclarationInType(td.resolveBinding(), mb);
if (md != null) {
// For polymorphic targets with unknown instance, we don't have concrete field bindings
Map<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
targets.add(new TargetMethod(md, targetFieldBindings));
}
}
}
}
}
// If the declaring class is a class (not an interface), or if the declared method itself is concrete/has a body,
// we should also include the declared method itself in the polymorphic targets.
MethodDeclaration declaredMd = findMethodDeclaration(mb);
if (declaredMd != null && declaredMd.getBody() != null) {
boolean alreadyAdded = false;
for (TargetMethod tm : targets) {
if (tm.declaration == declaredMd) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
Map<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
targets.add(new TargetMethod(declaredMd, targetFieldBindings));
}
}
}
// If no polymorphic subclass targets were found, fallback to the declared method itself
if (targets.isEmpty()) {
MethodDeclaration md = findMethodDeclaration(mb);
if (md != null) {
Map<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
targets.add(new TargetMethod(md, targetFieldBindings));
}
}
return targets;
}
private boolean isExpressionWrapping(Expression outer, Expression inner) {
Expression current = outer;
while (current != null) {
if (current == inner) return true;
if (current instanceof ParenthesizedExpression pe) {
current = pe.getExpression();
} else if (current instanceof CastExpression ce) {
current = ce.getExpression();
} else {
break;
}
}
return false;
}
private ControlFlowGraph.CfgNode findCfgNode(ControlFlowGraph cfg, ASTNode astNode) {
ASTNode current = astNode;
while (current != null) {
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
if (node.getAstNode() == current) {
return node;
}
}
current = current.getParent();
}
return null;
}
private boolean isTargetVariable(Expression expr, IVariableBinding targetVar, String targetName) {
if (expr instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding vb) {
if (targetVar != null && vb.getKey().equals(targetVar.getKey())) {
return true;
}
}
if (targetName != null && sn.getIdentifier().equals(targetName)) {
return true;
}
}
return false;
}
private void evaluateMethodMutations(
MethodDeclaration md,
List<?> arguments,
Map<IVariableBinding, Expression> fieldBindings) {
if (md == null || md.getBody() == null) return;
Map<IVariableBinding, Expression> currentParamValues = new HashMap<>();
List<?> parameters = md.parameters();
int count = Math.min(parameters.size(), arguments.size());
for (int i = 0; i < count; i++) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i);
IVariableBinding paramVb = svd.resolveBinding();
if (paramVb != null) {
Expression arg = (Expression) arguments.get(i);
currentParamValues.put(paramVb, arg);
}
}
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
Expression rhs = assignment.getRightHandSide();
IVariableBinding fieldVb = null;
if (lhs instanceof FieldAccess fa) {
if (fa.getExpression() instanceof ThisExpression) {
IBinding b = fa.getName().resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
} else if (lhs instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
if (fieldVb != null) {
Expression resolvedRhs = resolveParamValue(rhs, currentParamValues);
fieldBindings.put(fieldVb, resolvedRhs);
}
return super.visit(assignment);
}
});
}
private void findAndApplyMutations(
ASTNode node,
IVariableBinding targetVar,
String targetName,
Map<IVariableBinding, Expression> fieldBindings,
Map<IVariableBinding, Expression> paramBindings,
Set<ASTNode> visited,
int depth) {
node.accept(new ASTVisitor() {
@Override
public boolean visit(Assignment assignment) {
Expression lhs = assignment.getLeftHandSide();
Expression rhs = assignment.getRightHandSide();
IVariableBinding fieldVb = null;
if (lhs instanceof FieldAccess fa) {
if (isTargetVariable(fa.getExpression(), targetVar, targetName)) {
IBinding b = fa.getName().resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
} else if (lhs instanceof QualifiedName qn) {
if (isTargetVariable(qn.getQualifier(), targetVar, targetName)) {
IBinding b = qn.getName().resolveBinding();
if (b instanceof IVariableBinding vb && vb.isField()) {
fieldVb = vb;
}
}
}
if (fieldVb != null) {
List<Expression> resolvedRhs = getReachingDefinitions(rhs, visited, paramBindings, fieldBindings, depth + 1);
if (!resolvedRhs.isEmpty()) {
fieldBindings.put(fieldVb, resolvedRhs.get(0));
} else {
fieldBindings.put(fieldVb, rhs);
}
}
return super.visit(assignment);
}
@Override
public boolean visit(MethodInvocation mi) {
Expression receiver = mi.getExpression();
if (receiver != null && isTargetVariable(receiver, targetVar, targetName)) {
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, fieldBindings, depth + 1);
for (TargetMethod target : targets) {
if (target.declaration != null) {
evaluateMethodMutations(target.declaration, mi.arguments(), fieldBindings);
}
}
}
}
return super.visit(mi);
}
});
}
private void applyIntermediateMutations(
SimpleName receiverSimpleName,
ASTNode defNode,
ASTNode useNode,
MethodDeclaration enclosingMethod,
Map<IVariableBinding, Expression> fieldBindings,
Map<IVariableBinding, Expression> paramBindings,
Set<ASTNode> visited,
int depth) {
IBinding b = receiverSimpleName.resolveBinding();
IVariableBinding targetVar = (b instanceof IVariableBinding vb) ? vb : null;
String targetName = receiverSimpleName.getIdentifier();
ControlFlowGraph cfg = cfgCache.computeIfAbsent(enclosingMethod, CfgBuilder::build);
if (cfg == null) return;
ControlFlowGraph.CfgNode defCfgNode = findCfgNode(cfg, defNode);
ControlFlowGraph.CfgNode useCfgNode = findCfgNode(cfg, useNode);
if (defCfgNode != null && useCfgNode != null) {
Set<ControlFlowGraph.CfgNode> reachFromD = new HashSet<>();
Queue<ControlFlowGraph.CfgNode> queue = new LinkedList<>();
queue.add(defCfgNode);
reachFromD.add(defCfgNode);
while (!queue.isEmpty()) {
ControlFlowGraph.CfgNode curr = queue.poll();
for (ControlFlowGraph.CfgNode succ : curr.getSuccessors()) {
if (reachFromD.add(succ)) {
queue.add(succ);
}
}
}
Set<ControlFlowGraph.CfgNode> reachToU = new HashSet<>();
queue.clear();
queue.add(useCfgNode);
reachToU.add(useCfgNode);
while (!queue.isEmpty()) {
ControlFlowGraph.CfgNode curr = queue.poll();
for (ControlFlowGraph.CfgNode pred : curr.getPredecessors()) {
if (reachToU.add(pred)) {
queue.add(pred);
}
}
}
Set<ControlFlowGraph.CfgNode> pathNodes = new HashSet<>(reachFromD);
pathNodes.retainAll(reachToU);
List<ControlFlowGraph.CfgNode> orderedPathNodes = new ArrayList<>(pathNodes);
List<ControlFlowGraph.CfgNode> allNodes = cfg.getNodes();
orderedPathNodes.sort(Comparator.comparingInt(allNodes::indexOf));
for (ControlFlowGraph.CfgNode node : orderedPathNodes) {
if (node != defCfgNode && node != useCfgNode && node.getAstNode() != null) {
findAndApplyMutations(node.getAstNode(), targetVar, targetName, fieldBindings, paramBindings, visited, depth);
}
}
}
}
private MethodDeclaration findMethodDeclarationInType(ITypeBinding typeBinding, IMethodBinding mb) {
if (typeBinding == null || mb == null) return null;
// 1. Try to find it in the class hierarchy (including typeBinding itself)
ITypeBinding current = typeBinding;
while (current != null) {
MethodDeclaration md = findMethodInTypeDeclarationOnly(current, mb);
if (md != null) return md;
current = current.getSuperclass();
}
// 2. Try to find it in the interface hierarchy of typeBinding
Set<String> visited = new HashSet<>();
Queue<ITypeBinding> queue = new LinkedList<>();
queue.add(typeBinding);
while (!queue.isEmpty()) {
ITypeBinding tb = queue.poll();
if (tb == null) continue;
String key = tb.getKey();
if (key != null && !visited.add(key)) continue;
MethodDeclaration md = findMethodInTypeDeclarationOnly(tb, mb);
if (md != null) return md;
for (ITypeBinding interf : tb.getInterfaces()) {
queue.add(interf);
}
}
return null;
}
private MethodDeclaration findMethodInTypeDeclarationOnly(ITypeBinding typeBinding, IMethodBinding mb) {
String classFqn = typeBinding.getErasure().getQualifiedName();
if (classFqn != null && !classFqn.isEmpty()) {
TypeDeclaration td = context.getTypeDeclaration(classFqn);
if (td != null) {
for (MethodDeclaration md : td.getMethods()) {
IMethodBinding candidateMb = md.resolveBinding();
if (candidateMb != null && candidateMb.getKey().equals(mb.getKey())) {
return md;
}
}
for (MethodDeclaration md : td.getMethods()) {
if (md.getName().getIdentifier().equals(mb.getName()) &&
md.parameters().size() == mb.getParameterTypes().length) {
return md;
}
}
}
}
return null;
}
private MethodDeclaration findMethodDeclaration(IMethodBinding mb) {
if (mb == null) return null;
return findMethodDeclarationInType(mb.getDeclaringClass(), mb);
}
private List<ReturnStatement> findReturnStatements(ASTNode node) {
List<ReturnStatement> returns = new ArrayList<>();
node.accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement rs) {
returns.add(rs);
return super.visit(rs);
}
@Override
public boolean visit(LambdaExpression le) {
return false;
}
@Override
public boolean visit(AnonymousClassDeclaration acd) {
return false;
}
@Override
public boolean visit(TypeDeclarationStatement tds) {
return false;
}
});
return returns;
}
@Override
public String resolveValue(Expression expr, CodebaseContext context) {
if (expr == null) return null;
List<Expression> defs = getReachingDefinitions(expr);
if (defs.isEmpty()) {
return context.resolveExpression(expr);
}
String firstVal = null;
for (Expression def : defs) {
String val = context.resolveExpression(def);
if (val != null) {
if (firstVal == null) {
firstVal = val;
} else if (!firstVal.equals(val)) {
return null;
}
}
}
return firstVal != null ? firstVal : context.resolveExpression(expr);
}
}

View File

@@ -0,0 +1,31 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.Expression;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import java.util.List;
public class LegacyDataFlowModel implements DataFlowModel {
private final VariableTracer variableTracer;
public LegacyDataFlowModel(VariableTracer variableTracer) {
this.variableTracer = variableTracer;
}
@Override
public List<Expression> getReachingDefinitions(Expression expr) {
if (variableTracer == null || expr == null) {
return expr != null ? List.of(expr) : List.of();
}
return variableTracer.traceVariableAll(expr);
}
@Override
public String resolveValue(Expression expr, CodebaseContext context) {
if (expr == null) return null;
if (variableTracer == null) {
return context.resolveExpression(expr);
}
Expression traced = variableTracer.traceVariable(expr);
return context.resolveExpression(traced);
}
}

View File

@@ -0,0 +1,155 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
public class ReachingDefinitions {
private final ControlFlowGraph cfg;
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> inMap = new HashMap<>();
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> outMap = new HashMap<>();
public ReachingDefinitions(ControlFlowGraph cfg) {
this.cfg = cfg;
analyze();
}
private void analyze() {
List<ControlFlowGraph.CfgNode> cfgNodes = cfg.getNodes();
Map<ControlFlowGraph.CfgNode, ASTNode> nodeDefs = new HashMap<>();
Map<String, Set<ASTNode>> varToDefs = new HashMap<>();
for (ControlFlowGraph.CfgNode node : cfgNodes) {
ASTNode ast = node.getAstNode();
if (ast == null) continue;
ASTNode def = findDefinition(ast);
if (def != null) {
nodeDefs.put(node, def);
String varName = getDefinedVariableName(def);
if (varName != null) {
varToDefs.computeIfAbsent(varName, k -> new HashSet<>()).add(def);
}
}
}
for (ControlFlowGraph.CfgNode node : cfgNodes) {
inMap.put(node, new HashSet<>());
outMap.put(node, new HashSet<>());
}
boolean changed = true;
int iterations = 0;
int maxIterations = 5000;
while (changed && iterations < maxIterations) {
iterations++;
changed = false;
for (ControlFlowGraph.CfgNode node : cfgNodes) {
Set<ASTNode> newIn = new HashSet<>();
for (ControlFlowGraph.CfgNode pred : node.getPredecessors()) {
newIn.addAll(outMap.get(pred));
}
inMap.put(node, newIn);
Set<ASTNode> newOut = new HashSet<>(newIn);
ASTNode def = nodeDefs.get(node);
if (def != null) {
String varName = getDefinedVariableName(def);
if (varName != null) {
Set<ASTNode> allDefs = varToDefs.get(varName);
if (allDefs != null) {
newOut.removeAll(allDefs);
}
}
newOut.add(def);
}
Set<ASTNode> oldOut = outMap.put(node, newOut);
if (!newOut.equals(oldOut)) {
changed = true;
}
}
}
if (iterations >= maxIterations) {
// Log warning or print to stderr
System.err.println("Warning: ReachingDefinitions analysis reached max iterations (" + maxIterations + ") and terminated early.");
}
}
public Set<ASTNode> getReachingDefinitions(ASTNode usageNode, String varName) {
ControlFlowGraph.CfgNode cfgNode = findCfgNodeForUsage(usageNode);
if (cfgNode == null) {
return Collections.emptySet();
}
Set<ASTNode> reaching = new HashSet<>();
Set<ASTNode> inDefs = inMap.get(cfgNode);
if (inDefs != null) {
for (ASTNode def : inDefs) {
if (varName.equals(getDefinedVariableName(def))) {
reaching.add(def);
}
}
}
return reaching;
}
private ControlFlowGraph.CfgNode findCfgNodeForUsage(ASTNode usageNode) {
ASTNode current = usageNode;
while (current != null) {
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
if (node.getAstNode() == current) {
return node;
}
}
current = current.getParent();
}
return null;
}
private ASTNode findDefinition(ASTNode ast) {
if (ast instanceof VariableDeclarationStatement vds) {
if (!vds.fragments().isEmpty()) {
return (ASTNode) vds.fragments().get(0);
}
}
if (ast instanceof VariableDeclarationFragment) {
return ast;
}
if (ast instanceof SingleVariableDeclaration) {
return ast;
}
if (ast instanceof Assignment) {
return ast;
}
if (ast instanceof ExpressionStatement es) {
return findDefinition(es.getExpression());
}
return null;
}
public static String getDefinedVariableName(ASTNode def) {
if (def instanceof VariableDeclarationFragment frag) {
return frag.getName().getIdentifier();
}
if (def instanceof SingleVariableDeclaration svd) {
return svd.getName().getIdentifier();
}
if (def instanceof Assignment assign) {
if (assign.getLeftHandSide() instanceof SimpleName sn) {
return sn.getIdentifier();
}
}
return null;
}
public static Expression getDefinitionExpression(ASTNode def) {
if (def instanceof VariableDeclarationFragment frag) {
return frag.getInitializer();
}
if (def instanceof Assignment assign) {
return assign.getRightHandSide();
}
return null;
}
}

View File

@@ -0,0 +1,119 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.model.State;
import org.eclipse.jdt.core.dom.*;
public class StateResolver {
private final CodebaseContext context;
public StateResolver(CodebaseContext context) {
this.context = context;
}
public State resolveState(Expression expr, CompilationUnit cu) {
if (expr == null)
return null;
// 1. Check for constants
String resolvedValue = context.getConstantResolver().resolve(expr, context);
if (resolvedValue != null) {
return State.of(expr.toString(), resolvedValue);
}
// 2. Check for @Value fields (SimpleName)
if (expr instanceof SimpleName sn) {
String fieldName = sn.getIdentifier();
TypeDeclaration td = findEnclosingType(sn);
if (td != null) {
String value = findValueFromField(td, fieldName);
if (value != null) {
return State.of(fieldName, value);
}
}
}
if (expr instanceof StringLiteral sl)
return State.of(sl.getLiteralValue());
String raw = expr.toString();
if (expr instanceof QualifiedName qn) {
String full = resolveQualifiedName(qn, cu);
return State.of(raw, full);
}
if (expr instanceof SimpleName sn) {
String full = resolveSimpleName(sn, cu);
return State.of(raw, full);
}
return State.of(raw);
}
private String findValueFromField(TypeDeclaration td, String fieldName) {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(fieldName)) {
// Check for @Value annotation
for (Object mod : fd.modifiers()) {
if (mod instanceof Annotation ann) {
String name = ann.getTypeName().getFullyQualifiedName();
if (name.endsWith("Value")) {
return extractAnnotationValue(ann);
}
}
}
}
}
}
}
return null;
}
private String extractAnnotationValue(Annotation ann) {
if (ann instanceof SingleMemberAnnotation sma) {
return stripQuotes(sma.getValue().toString());
} else if (ann instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if ("value".equals(pair.getName().getIdentifier())) {
return stripQuotes(pair.getValue().toString());
}
}
}
return null;
}
private String stripQuotes(String s) {
if (s == null) return null;
return s.replaceAll("^\"|\"$", "");
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
String qualifier = qn.getQualifier().toString();
String name = qn.getName().getIdentifier();
TypeDeclaration td = context.getTypeDeclaration(qualifier, cu);
if (td != null)
return context.getFqn(td) + "." + name;
return qn.getFullyQualifiedName();
}
private String resolveSimpleName(SimpleName sn, CompilationUnit cu) {
String name = sn.getIdentifier();
if (cu == null)
return name;
for (Object impObj : cu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
String impName = imp.getName().getFullyQualifiedName();
if (impName.endsWith("." + name))
return impName;
}
return name;
}
}

View File

@@ -57,6 +57,9 @@ public class ExporterCommand implements Callable<Integer> {
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
private boolean debug;
@Option(names = {"--resolve-classpath"}, description = "Automatically run Maven/Gradle to resolve full classpath dependencies for perfect AST bindings.", defaultValue = "false")
private boolean resolveClasspath;
@Override
public Integer call() throws Exception {
var out = spec.commandLine().getOut();
@@ -87,7 +90,7 @@ public class ExporterCommand implements Callable<Integer> {
if (jsonFile != null) {
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
} else {
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, eventFormat, stateFormat);
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, resolveClasspath);
}
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
return 0;

View File

@@ -45,6 +45,7 @@ public class ExportService {
new EntryPointEnricher(),
new PropertyEnricher(),
new CallChainEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
)));
}
@@ -53,22 +54,22 @@ public class ExportService {
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
}
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
}
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
}
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat);
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat, false);
}
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean resolveClasspath) throws IOException {
CodebaseContext context = new CodebaseContext();
context.setActiveProfiles(activeProfiles);
@@ -94,6 +95,18 @@ public class ExportService {
.collect(Collectors.toList());
log.info("Setting JDT sourcepath: {}", sourcepaths);
context.setSourcepath(sourcepaths);
if (resolveClasspath) {
click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver classpathResolver = new click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver();
List<String> dynamicClasspath = classpathResolver.resolveClasspath(projectRoot);
if (!dynamicClasspath.isEmpty()) {
context.setClasspath(dynamicClasspath);
log.info("Injected {} external JARs into JDT ASTParser", dynamicClasspath.size());
}
} else {
log.info("Dynamic classpath resolution disabled. Use --resolve-classpath to enable.");
}
context.setResolveBindings(true);
Path hintsFile = inputDir.resolve("hints.json");

View File

@@ -143,6 +143,12 @@ public class RegressionTest {
root.resolve("state_machines/complex_multi_module_sm"),
Path.of("src/test/resources/golden/StateMachineConfig"),
"StateMachineConfig"
),
new TestScenario(
"Polymorphic Events Sample",
root.resolve("state_machines/polymorphic_events_sample"),
Path.of("src/test/resources/golden/PolymorphicStateMachineConfiguration"),
"PolymorphicStateMachineConfiguration"
)
);
}
@@ -155,7 +161,7 @@ public class RegressionTest {
if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
if (tempDir == null) System.out.println("tempDir is NULL");
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles());
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
// Find the generated directory (it might be named with FQN)
List<Path> generatedDirs;

View File

@@ -9,6 +9,7 @@ import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -88,4 +89,42 @@ public class ResolutionRobustnessTest {
assertThat(context.getTypeDeclaration("p1.X")).isNotNull();
assertThat(context.getTypeDeclaration("p2.X")).isNotNull();
}
@Test
void testJdtBindingsResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Base.java"),
"package com.foo;\n" +
"public class Base {\n" +
" public void run() {}\n" +
"}"
);
Files.writeString(comFoo.resolve("Child.java"),
"package com.foo;\n" +
"public class Child extends Base {\n" +
" public void execute() {\n" +
" run();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
assertThat(childTd).isNotNull();
// Resolve the binding for Child class
org.eclipse.jdt.core.dom.ITypeBinding binding = childTd.resolveBinding();
assertThat(binding).isNotNull();
assertThat(binding.getQualifiedName()).isEqualTo("com.foo.Child");
// Verify we can get the superclass binding
org.eclipse.jdt.core.dom.ITypeBinding superBinding = binding.getSuperclass();
assertThat(superBinding).isNotNull();
assertThat(superBinding.getQualifiedName()).isEqualTo("com.foo.Base");
}
}

View File

@@ -4,7 +4,7 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.CallGraphBuilder;
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine;
import click.kamil.springstatemachineexporter.analysis.service.GenericEventDetector;
import click.kamil.springstatemachineexporter.analysis.service.SpringMvcDetector;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
@@ -76,7 +76,7 @@ public class InheritedEventDetectionTest {
assertThat(tp.getEvent()).isEqualTo("event");
// Build Call Chains
CallGraphBuilder callGraphBuilder = new CallGraphBuilder(context);
HeuristicCallGraphEngine callGraphBuilder = new HeuristicCallGraphEngine(context);
List<CallChain> chains = callGraphBuilder.findChains(entryPoints, triggers);
assertThat(chains).describedAs("Should link ChildController.trigger to BaseController.send").hasSize(1);

View File

@@ -145,6 +145,36 @@ class TransitionLinkerEnricherTest {
@Test
void shouldMatchBaseEventDespiteScrapedPolyEvents() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
// State machine uses the base event generically
t1.setEvent(Event.of("BaseEvent", "BaseEvent"));
// Trigger point uses "BaseEvent", but polyEvents scraped some implementation/constants
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("BaseEvent")
.polymorphicEvents(List.of("EVENT_A", "EVENT_B"))
.build())
.contextMachineId("testMachine")
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
// It should match because smEvent matches triggerEvent directly, overriding the failed poly search
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
}
@Test
void shouldNotMatchArbitraryGetXMethodWildcard() {
Transition t1 = new Transition();
@@ -192,4 +222,190 @@ class TransitionLinkerEnricherTest {
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
}
}
@Test
void shouldNotFalsePositiveOnSubstringContains() {
// This test proves that the heuristic is entirely removed and false positives are eliminated
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
// Trigger point sends "PAYMENT" which contains "PAY". Under the old heuristics, this would falsely match!
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("PAYMENT").build())
.build();
AnalysisResult result = AnalysisResult.builder()
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
// It should NOT match, because "PAYMENT" != "PAY" strictly.
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
}
@Test
void shouldFilterCrossStateMachineRoutingByVertical() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
.methodChain(List.of(
"com.example.electronics.ElectronicsOrderController.post()",
"com.example.electronics.ElectronicsOrderService.processOrderEvent()"
))
.build();
// Testing against Electronics SM - should match
AnalysisResult resultElectronics = AnalysisResult.builder()
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(resultElectronics, null, null);
CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
// Testing against ComputerStore SM - should NOT match because chain has Electronics service
AnalysisResult resultComputer = AnalysisResult.builder()
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration") // the non-electronics one
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(resultComputer, null, null);
CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
assertThat(updatedChainComp.getMatchedTransitions()).isNullOrEmpty();
}
@Test
void shouldAllowSharedServicesToRouteToMultipleStateMachines() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PROCESSED", "PROCESSED")));
t1.setEvent(Event.of("PROCESS", "PROCESS"));
// Chain contains NO vertical prefix (e.g. CommonOrderService)
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("PROCESS").build())
.methodChain(List.of(
"com.example.CommonOrderController.post()",
"com.example.CommonOrderService.processOrderEvent()"
))
.build();
AnalysisResult resultElectronics = AnalysisResult.builder()
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(resultElectronics, null, null);
CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
// Should match Electronics because it's a shared service (no computerstore service in path)
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
AnalysisResult resultComputer = AnalysisResult.builder()
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(resultComputer, null, null);
CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
// Should ALSO match ComputerStore because it's a shared service
assertThat(updatedChainComp.getMatchedTransitions()).hasSize(1);
}
@Test
void shouldFilterCrossStateMachineRoutingWithMultipleVerticals() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
// Chain 1: Electronics
CallChain chainElec = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
.methodChain(List.of("com.example.electronics.ElectronicsOrderController.post()"))
.build();
// Chain 2: Furniture
CallChain chainFurn = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
.methodChain(List.of("com.example.furniture.FurnitureOrderController.post()"))
.build();
// Chain 3: Groceries
CallChain chainGroc = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
.methodChain(List.of("com.example.groceries.GroceriesOrderController.post()"))
.build();
// SM 1: Electronics
AnalysisResult resElec = AnalysisResult.builder()
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
.build();
// SM 2: Furniture
AnalysisResult resFurn = AnalysisResult.builder()
.name("com.example.furniture.FurnitureOwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
.build();
// SM 3: Groceries
AnalysisResult resGroc = AnalysisResult.builder()
.name("com.example.groceries.GroceriesStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
.build();
// SM 4: Computer Store (The catch-all base one in computerstore package)
AnalysisResult resComp = AnalysisResult.builder()
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
.build();
// Test Electronics SM - Should only keep Electronics Chain
enricher.enrich(resElec, null, null);
assertThat(resElec.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1); // Elec
assertThat(resElec.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
assertThat(resElec.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
// Test Furniture SM - Should only keep Furniture Chain
enricher.enrich(resFurn, null, null);
assertThat(resFurn.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
assertThat(resFurn.getMetadata().getCallChains().get(1).getMatchedTransitions()).hasSize(1); // Furn
assertThat(resFurn.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
// Test Groceries SM - Should only keep Groceries Chain
enricher.enrich(resGroc, null, null);
assertThat(resGroc.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
assertThat(resGroc.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
assertThat(resGroc.getMetadata().getCallChains().get(2).getMatchedTransitions()).hasSize(1); // Groc
// Test ComputerStore SM - Should reject ALL because none belong to computerstore
enricher.enrich(resComp, null, null);
assertThat(resComp.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
assertThat(resComp.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
assertThat(resComp.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
}
}

View File

@@ -0,0 +1,91 @@
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.model.Event;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class HeuristicEventMatchingEngineTest {
private final HeuristicEventMatchingEngine engine = new HeuristicEventMatchingEngine();
@Test
void shouldMatchExactFqn() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("com.example.OrderEvents.PAY").build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldMatchEnumQualifierToFqn() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("OrderEvents.PAY").build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldPreventCrossEnumContamination() {
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("OrderEvents.PAY").build();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
void shouldPreventCrossEnumContaminationWithPolymorphicEvents() {
Event smEvent = Event.of("PAY", "com.example.InvoiceEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("OrderEvents.PAY")).build();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
void shouldMatchPolymorphicEnumQualifierToFqn() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("OrderEvents.PAY")).build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldMatchRawStringToFqn() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("PAY").build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldMatchRawStringToRawString() {
Event smEvent = Event.of("PAY", "PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("PAY").build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldMatchPolymorphicRawStringToFqn() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("PAY")).build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@Test
void shouldMatchBidirectionalFqnAndShortName() {
// Case 1: smEvent is short, trigger event is FQN
Event smEvent1 = Event.of("PAY", "OrderEvents.PAY");
TriggerPoint triggerPoint1 = TriggerPoint.builder().event("com.example.OrderEvents.PAY").build();
assertThat(engine.matches(smEvent1, triggerPoint1)).isTrue();
// Case 2: smEvent is short, polymorphic event is FQN
Event smEvent2 = Event.of("PAY", "OrderEvents.PAY");
TriggerPoint triggerPoint2 = TriggerPoint.builder().event("getType()").polymorphicEvents(List.of("com.example.OrderEvents.PAY")).build();
assertThat(engine.matches(smEvent2, triggerPoint2)).isTrue();
}
}

View File

@@ -0,0 +1,103 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
@Tag("heuristic_bean_resolution")
public class HeuristicBeanResolutionEngineTest {
private final HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
@Test
public void testDeepPackageDivergenceMismatch() {
// This is the bug case: sharing a deep root, but diverging into different domains
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.corp.division.project.orders.OrderService.pay()"))
.build();
String machineName = "com.acme.corp.division.project.payments.PaymentStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// They share com.acme.corp.division.project, but diverge into orders vs payments
// This should be a strong mismatch, so it should return false
assertFalse(result, "Deep package divergence into different domains should be rejected");
}
@Test
public void testSameDomainMatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.ecommerce.orders.OrderService.process()"))
.build();
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Same exact domain, should match
assertTrue(result, "Same domain should be accepted");
}
@Test
public void testSubPackageMatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.ecommerce.orders.impl.OrderServiceImpl.process()"))
.build();
String machineName = "com.acme.ecommerce.orders.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Subpackage should match
assertTrue(result, "Sub-packages of the same domain should be accepted");
}
@Test
public void testSharedDomainTermMatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.orders.web.OrderController.submit()"))
.build();
String machineName = "com.acme.orders.service.OrderStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Different subpackages, but they share the "order" domain term and common prefix
assertTrue(result, "Divergent packages that share a domain term should be accepted");
}
@Test
public void testExplicitTargetVariableMatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.common.GlobalTrigger.fire()"))
.contextMachineId("paymentStateMachine") // Explicitly targets payment
.build();
String machineName = "com.acme.corp.payments.PaymentStateMachine";
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Mismatched domains, but explicit variable targeting works
assertTrue(result, "Explicit variable target matching the machine name should be accepted");
}
@Test
public void testExplicitTargetVariableMismatch() {
CallChain chain = CallChain.builder()
.methodChain(Arrays.asList("com.acme.common.GlobalTrigger.fire()"))
.contextMachineId("paymentStateMachine") // Explicitly targets payment
.build();
String machineName = "com.acme.corp.orders.OrderStateMachine"; // But this is Order
boolean result = engine.isRoutedToCorrectMachine(chain, machineName);
// Explicit variable targeting completely conflicts
assertFalse(result, "Explicit variable target mismatching the machine name should be rejected");
}
}

View File

@@ -152,4 +152,334 @@ public class ConstantResolverTest {
String result = resolver.resolve(returnExpr, context);
assertThat(result).isEqualTo("${app.path}");
}
@Test
void testConstructorAssignmentAndFieldAccess(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("EventDto.java"),
"package com.example;\n" +
"public class EventDto {\n" +
" private String eventType;\n" +
" public EventDto() {\n" +
" this.eventType = \"PAYMENT_SUCCESS\";\n" +
" }\n" +
" public String getEventType() {\n" +
" return this.eventType;\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.example.EventDto");
MethodDeclaration method = td.getMethods()[1]; // getEventType
ReturnStatement rs = (ReturnStatement) method.getBody().statements().get(0);
Expression returnExpr = rs.getExpression(); // this.eventType
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(returnExpr, context);
assertThat(result).isEqualTo("PAYMENT_SUCCESS");
}
@Test
void testMethodEvaluationWithNestedBlocksAndVars(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("Logic.java"),
"package com.example;\n" +
"public class Logic {\n" +
" public String process(String event) {\n" +
" String myVar = event;\n" +
" if (myVar != null) {\n" +
" return myVar;\n" +
" }\n" +
" return \"DEFAULT\";\n" +
" }\n" +
"}");
Files.writeString(dir.resolve("Caller.java"),
"package com.example;\n" +
"public class Caller {\n" +
" public void call() {\n" +
" Logic logic = new Logic();\n" +
" String result = logic.process(\"MY_EVENT\");\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process("MY_EVENT")
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(mi, context);
assertThat(result).isEqualTo("MY_EVENT");
}
@Test
void testMethodShadowingReturnsParameter(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("Logic.java"),
"package com.example;\n" +
"public class Logic {\n" +
" public String a = \"OLD_FIELD\";\n" +
" public String process(String a) {\n" + // shadowing
" this.a = \"NEW_FIELD\";\n" +
" return a;\n" + // should return the parameter
" }\n" +
"}");
Files.writeString(dir.resolve("Caller.java"),
"package com.example;\n" +
"public class Caller {\n" +
" public void call() {\n" +
" Logic logic = new Logic();\n" +
" String result = logic.process(\"PARAM_VALUE\");\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(mi, context);
assertThat(result).isEqualTo("PARAM_VALUE");
}
@Test
void testMethodShadowingReturnsField(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("Logic.java"),
"package com.example;\n" +
"public class Logic {\n" +
" public String a = \"OLD_FIELD\";\n" +
" public String process(String a) {\n" + // shadowing
" this.a = \"NEW_FIELD_VALUE\";\n" +
" return this.a;\n" + // should return the field
" }\n" +
"}");
Files.writeString(dir.resolve("Caller.java"),
"package com.example;\n" +
"public class Caller {\n" +
" public void call() {\n" +
" Logic logic = new Logic();\n" +
" String result = logic.process(\"PARAM_VALUE\");\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(mi, context);
assertThat(result).isEqualTo("NEW_FIELD_VALUE");
}
@Test
void testMethodImplicitFieldAssignment(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("Logic.java"),
"package com.example;\n" +
"public class Logic {\n" +
" public String a = \"OLD_FIELD\";\n" +
" public String process(String p) {\n" +
" a = p;\n" + // implicit field assignment
" return this.a;\n" +
" }\n" +
"}");
Files.writeString(dir.resolve("Caller.java"),
"package com.example;\n" +
"public class Caller {\n" +
" public void call() {\n" +
" Logic logic = new Logic();\n" +
" String result = logic.process(\"MY_EVENT\");\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(mi, context);
assertThat(result).isEqualTo("MY_EVENT");
}
@Test
void testZeroArgMethodEvaluation(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("Logic.java"),
"package com.example;\n" +
"public class Logic {\n" +
" public String a = \"MY_CONSTANT\";\n" +
" public String getEvent() {\n" +
" return this.a;\n" +
" }\n" +
"}");
Files.writeString(dir.resolve("Caller.java"),
"package com.example;\n" +
"public class Caller {\n" +
" public void call() {\n" +
" Logic logic = new Logic();\n" +
" String result = logic.getEvent();\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.getEvent()
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(mi, context);
assertThat(result).isEqualTo("MY_CONSTANT");
}
/**
* When a switch method is called with a <em>known</em> constant argument, {@code evaluateMethodOutput}
* should evaluate the switch and return only the matching branch.
*
* <p>Regression guard: before the {@code SwitchStatement.visit} fix, the ASTVisitor would also descend
* into child {@code ReturnStatement} nodes after the switch was already handled, potentially overwriting
* a correct result with a wrong one.
*/
@Test
void testOldStyleSwitchResolvesCorrectBranchForKnownConstant(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("Classifier.java"),
"package com.example;\n" +
"public class Classifier {\n" +
" public OrderEvents classify(String q) {\n" +
" switch (q) {\n" +
" case \"a\": return OrderEvents.A8;\n" +
" case \"b\": return OrderEvents.B2;\n" +
" default: return OrderEvents.DEF;\n" +
" }\n" +
" }\n" +
"}\n" +
"enum OrderEvents { A8, B2, DEF }");
Files.writeString(dir.resolve("Caller.java"),
"package com.example;\n" +
"public class Caller {\n" +
" public void call() {\n" +
" Classifier c = new Classifier();\n" +
" OrderEvents r = c.classify(\"a\");\n" + // known constant → should match case "a"
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
MethodDeclaration callMethod = callerTd.getMethods()[0];
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify("a")
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(mi, context);
// Only the matching branch should be returned, not all branches
assertThat(result).isEqualTo("OrderEvents.A8");
}
/**
* When a switch method is called with a <em>runtime</em> variable that cannot be statically resolved,
* {@code evaluateMethodOutput} must return {@code null} so the resolver falls back to the return-type
* enum set (covering all branches).
*
* <p>Regression: before the fix, {@code SwitchStatement.visit} returned {@code super.visit(ss)} (true),
* causing child {@code ReturnStatement} nodes inside the switch to fire. The first return — e.g.
* {@code return OrderEvents.A8} — was captured as if it were the only possible result, silently
* dropping the other branches. After the fix the visitor returns {@code false}, preventing child
* traversal, so the resolver correctly falls back to the full ENUM_SET.
*/
@Test
void testOldStyleSwitchWithUnknownVariableReturnsFallbackEnumSet(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("Classifier.java"),
"package com.example;\n" +
"public class Classifier {\n" +
" public OrderEvents classify(String q) {\n" +
" switch (q) {\n" +
" case \"a\": return OrderEvents.A8;\n" +
" case \"b\": return OrderEvents.B2;\n" +
" default: return OrderEvents.DEF;\n" +
" }\n" +
" }\n" +
"}\n" +
"enum OrderEvents { A8, B2, DEF }");
Files.writeString(dir.resolve("Caller.java"),
"package com.example;\n" +
"public class Caller {\n" +
" public void call(String x) {\n" + // x is a runtime parameter — not a constant
" Classifier c = new Classifier();\n" +
" OrderEvents r = c.classify(x);\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
MethodDeclaration callMethod = callerTd.getMethods()[0];
VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify(x)
ConstantResolver resolver = new ConstantResolver();
String result = resolver.resolve(mi, context);
// Must cover ALL branches via ENUM_SET — not just the first one ("A8")
assertThat(result).startsWith("ENUM_SET:");
assertThat(result).contains("OrderEvents.A8");
assertThat(result).contains("OrderEvents.B2");
assertThat(result).contains("OrderEvents.DEF");
}
}

View File

@@ -0,0 +1,78 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class AnonymousClassBugTest {
@Test
void shouldResolveEventThroughAnonymousClass() throws IOException {
String source = """
package com.example;
public class MyController {
private StateMachine machine;
public void process() {
// Supplying the event via an anonymous class
fireEvent(new EventProvider() {
@Override
public TransitionEnum getEvent() {
return TransitionEnum.STATE_A;
}
});
}
private void fireEvent(EventProvider provider) {
machine.fire(provider.getEvent());
}
}
interface EventProvider {
TransitionEnum getEvent();
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum TransitionEnum { STATE_A, STATE_B }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_anonymous");
Files.writeString(tempDir.resolve("Config.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.MyController")
.methodName("process")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("Poly events from anonymous class: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
// It SHOULD resolve to STATE_A by tracking the anonymous class implementation
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_A");
}
}

View File

@@ -0,0 +1,61 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class AnonymousClassTests {
@TempDir
Path tempDir;
@Test
void shouldResolveEventFromAnonymousClass() throws Exception {
String source = """
package com.example;
public class AnonymousService {
private EventService service;
public void process() {
MyEvent event = new MyEvent() {
@Override
public String getEvent() {
return "ANON_EVENT";
}
};
service.trigger(event.getEvent());
}
}
class EventService {
public void trigger(String ev) {}
}
class MyEvent {
public String getEvent() { return "BASE_EVENT"; }
}
""";
Files.writeString(tempDir.resolve("AnonymousService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("AnonymousService.java")
.sourceModule("com.example")
.event("event.getEvent()")
.lineNumber(12)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.AnonymousService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
assertThat(result.getPolymorphicEvents()).contains("ANON_EVENT");
}
}

View File

@@ -0,0 +1,79 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class BuilderBugTest {
@Test
void shouldResolveEventFromBuilder() throws IOException {
String source = """
package com.example;
public class Controller {
private StateMachine machine;
public void process() {
EventWrapper wrapper = EventWrapper.builder()
.event(TransitionEnum.STATE_X)
.build();
machine.fire(wrapper.getEvent());
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
class EventWrapper {
private TransitionEnum event;
public TransitionEnum getEvent() { return event; }
public static Builder builder() { return new Builder(); }
public static class Builder {
private TransitionEnum event;
public Builder event(TransitionEnum e) { this.event = e; return this; }
public EventWrapper build() {
EventWrapper w = new EventWrapper();
w.event = this.event;
return w;
}
}
}
enum TransitionEnum { STATE_X, STATE_Y }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_builder");
Files.writeString(tempDir.resolve("Config.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.Controller")
.methodName("process")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("Poly events: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("TransitionEnum.STATE_X");
}
}

View File

@@ -0,0 +1,69 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class BuilderLocalVariableTest {
@Test
void shouldTraceLocalVariableInBuilderPattern(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent() {
OrderEvents e = OrderEvents.PAY;
RichOrderEvent event = new RichOrderEvent();
event.setType(e);
service.updateOrderState(event.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
public void setType(OrderEvents type) { this.type = type; }
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
System.out.println("POLY EVENTS FOUND: " + chain.getTriggerPoint().getPolymorphicEvents());
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.PAY");
}
}

View File

@@ -0,0 +1,54 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class CollectionIndexingTests {
@TempDir
Path tempDir;
@Test
void shouldResolveEventFromArrayAccess() throws Exception {
String source = """
package com.example;
public class ArrayService {
private EventService service;
public void process() {
String[] events = new String[] {"EVENT_1", "EVENT_2"};
service.trigger(events[0]);
}
}
class EventService {
public void trigger(String ev) {}
}
""";
Files.writeString(tempDir.resolve("ArrayService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("ArrayService.java")
.sourceModule("com.example")
.event("events[0]")
.lineNumber(7)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.ArrayService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
// When using array indexing, we should ideally extract the indexed element or at least all elements in the array
assertThat(result.getPolymorphicEvents()).contains("EVENT_1");
}
}

View File

@@ -0,0 +1,412 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ConstructorInvocationTest {
@Test
void shouldNotBlindlyAddAllConstructorArguments(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
private String irrelevantInfo;
public RichOrderEvent() {
this(OrderEvents.PAY, "DEFAULT_INFO");
}
public RichOrderEvent(OrderEvents type, String info) {
this.type = type;
this.irrelevantInfo = info;
}
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.contains("OrderEvents.PAY")
.doesNotContain("DEFAULT_INFO");
}
@Test
void shouldHandleOverloadedConstructorsGracefully(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
private String irrelevantInfo;
private int count;
public RichOrderEvent() {
this(OrderEvents.PAY, "DEFAULT_INFO");
}
// Overloaded constructor 1
public RichOrderEvent(OrderEvents type, String info) {
this.type = type;
this.irrelevantInfo = info;
}
// Overloaded constructor 2 (same number of arguments!)
public RichOrderEvent(String info, int count) {
this.irrelevantInfo = info;
this.count = count;
// type is not set here
}
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.contains("OrderEvents.PAY")
.doesNotContain("DEFAULT_INFO")
.doesNotContain("count");
}
@Test
void shouldHandleChainedConstructorDelegationGracefully(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class BaseEvent {
protected OrderEvents type;
public BaseEvent(OrderEvents type) {
this.type = type;
}
}
class RichOrderEvent extends BaseEvent {
public RichOrderEvent() {
this(OrderEvents.PAY);
}
public RichOrderEvent(OrderEvents type) {
this(type, "DEFAULT_INFO");
}
public RichOrderEvent(OrderEvents type, String info) {
super(type);
}
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.contains("OrderEvents.PAY")
.doesNotContain("DEFAULT_INFO");
}
@Test
void shouldHandleFieldAccessGracefully(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
public RichOrderEvent() {
this(OrderEvents.PAY);
}
public RichOrderEvent(OrderEvents type) {
this.type = type;
}
public OrderEvents getType() {
return this.type; // Specifically testing FieldAccess
}
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.contains("OrderEvents.PAY");
}
@Test
void shouldTraceThroughWrappedConstructorArguments(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
import java.util.Objects;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
public RichOrderEvent() {
this(Objects.requireNonNull(OrderEvents.PAY));
}
public RichOrderEvent(OrderEvents type) {
this((OrderEvents) type, "DEFAULT_INFO");
}
public RichOrderEvent(OrderEvents type, String info) {
this.type = Objects.requireNonNull(type);
}
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.contains("OrderEvents.PAY");
}
@Test
void shouldTraceThroughComplexOverloadedConstructors(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(EnterpriseEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class EnterpriseEvent {
private OrderEvents type;
private String info;
public EnterpriseEvent() {
this(OrderEvents.PAY);
}
// Matches by parameter count but wrong type!
public EnterpriseEvent(String info) {
this.info = info;
}
public EnterpriseEvent(OrderEvents type) {
this("info", type);
}
// Target of previous constructor
public EnterpriseEvent(String info, OrderEvents type) {
this(type, info, null);
}
// Final destination
public EnterpriseEvent(OrderEvents type, String info, Object extra) {
this.type = type;
this.info = info;
}
public OrderEvents getType() { return type; }
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.contains("OrderEvents.PAY");
}
}

View File

@@ -0,0 +1,72 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ConstructorLocalVariableTest {
@Test
void shouldTraceLocalVariableInConstructor(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
public RichOrderEvent() {
OrderEvents e = OrderEvents.PAY;
this.type = e;
}
public OrderEvents getType() {
return type;
}
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.PAY");
}
}

View File

@@ -0,0 +1,78 @@
package click.kamil.springstatemachineexporter.analysis.service;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("dynamic_classpath_resolver")
class DynamicClasspathResolverTest {
@Test
void shouldResolveMavenClasspathDynamically(@TempDir Path tempDir) throws IOException {
// Create dummy pom.xml
Files.createFile(tempDir.resolve("pom.xml"));
// Mock the resolver so it doesn't actually run maven, but writes the expected output file
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
@Override
protected void runProcess(ProcessBuilder pb) throws IOException {
// Pretend maven executed and generated the cp.txt
String cp = "/fake/m2/repo/spring-core.jar" + File.pathSeparator + "/fake/m2/repo/spring-context.jar";
String outputFileArg = pb.command().stream()
.filter(arg -> arg.startsWith("-Dmdep.outputFile="))
.findFirst()
.orElseThrow();
Path outputFile = Path.of(outputFileArg.substring("-Dmdep.outputFile=".length()));
Files.writeString(outputFile, cp);
}
};
List<String> classpath = resolver.resolveClasspath(tempDir);
assertThat(classpath).containsExactly(
"/fake/m2/repo/spring-core.jar",
"/fake/m2/repo/spring-context.jar"
);
}
@Test
void shouldResolveGradleClasspathDynamically(@TempDir Path tempDir) throws IOException {
// Create dummy build.gradle
Files.createFile(tempDir.resolve("build.gradle"));
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
@Override
protected String runProcessAndGetOutput(ProcessBuilder pb) {
// Pretend gradle executed and printed the marker
String cp = "/fake/gradle/caches/spring-core.jar" + File.pathSeparator + "/fake/gradle/caches/spring-context.jar";
return "Some noisy gradle output\n" +
"SME_CLASSPATH_MARKER:" + cp + "\n" +
"BUILD SUCCESSFUL\n";
}
};
List<String> classpath = resolver.resolveClasspath(tempDir);
assertThat(classpath).containsExactly(
"/fake/gradle/caches/spring-core.jar",
"/fake/gradle/caches/spring-context.jar"
);
}
@Test
void shouldReturnEmptyListIfNoBuildFileFound(@TempDir Path tempDir) {
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> classpath = resolver.resolveClasspath(tempDir);
assertThat(classpath).isEmpty();
}
}

View File

@@ -0,0 +1,123 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class EnterpriseBugsTest {
@Test
void shouldResolveStaticImportAndAvoidMismatchedConstructorEnumArgs(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
import static com.example.EventUtils.assertMatchingCancelEventType;
public class OrderController {
private OrderService service;
public void handleCancel(Cancellation.Sender sender) {
service.process(new OrderCancelledEvent(sender).getType());
}
}
class OrderService {
public void process(CommonOrderEvent event) {}
}
class OrderCancelledEvent {
private CommonOrderEvent type;
public OrderCancelledEvent(Cancellation.Sender sender) {
this.type = assertMatchingCancelEventType(sender);
}
public CommonOrderEvent getType() { return type; }
}
class EventUtils {
public static CommonOrderEvent assertMatchingCancelEventType(Cancellation.Sender sender) {
return switch(sender) {
case registry_corp -> CommonOrderEvent.CANCELLED_BY_registry_corp;
default -> null;
};
}
}
class Cancellation {
public enum Sender { registry_corp }
}
enum CommonOrderEvent { CANCELLED_BY_registry_corp }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("handleCancel")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("process")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
TriggerPoint resolvedTp = chains.get(0).getTriggerPoint();
// Assert Bug 3: assertMatchingCancelEventType method is resolved via static import
// Assert Bug 2: Cancellation.Sender.registry_corp is NOT extracted as an event argument
assertThat(resolvedTp.getPolymorphicEvents())
.as("Polymorphic events found: %s", resolvedTp.getPolymorphicEvents())
.contains("CommonOrderEvent.CANCELLED_BY_registry_corp")
.doesNotContain("registry_corp", "Cancellation.Sender.registry_corp");
}
@Test
void shouldAcceptConnectorCallChainUsingTriggerPointDomainMatch() {
HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.className("project.shop.OrderService")
.build())
.methodChain(List.of("project.enterprise.connector.org.OrgOrderControllerImpl.accept"))
.build();
// Assert Bug 4: Connector entry points targeting shop SM trigger point are accepted
boolean isMatched = engine.isRoutedToCorrectMachine(chain, "project.shop.OrderStateMachine");
assertThat(isMatched).isTrue();
}
@Test
void shouldResolveClassConstantToSimpleNameWithoutFQNDuplicates(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public enum CommonOrderEvent { ACCEPTED_BY_corp }
""";
Files.writeString(tempDir.resolve("CommonOrderEvent.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
List<String> resolved = engine.resolveClassConstantReturns("com.example.CommonOrderEvent.ACCEPTED_BY_corp", context, null);
// Assert Bug 6: resolves cleanly to simple name (since prefix is a known type)
assertThat(resolved).containsExactly("CommonOrderEvent.ACCEPTED_BY_corp");
}
}

View File

@@ -0,0 +1,132 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("control_flow_sibling_avoidance")
class GenericEventDetectorControlFlowTest {
@Test
void shouldDetectStateFromDirectIfWrapper(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
private StateMachine sm;
public void processOrder(OrderState state) {
if (state == OrderState.PENDING) {
sm.sendEvent(OrderEvent.PAY);
}
}
}
class StateMachine {
public void sendEvent(OrderEvent e) {}
}
enum OrderState { PENDING }
enum OrderEvent { PAY }
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
assertThat(triggers).hasSize(1);
TriggerPoint tp = triggers.get(0);
assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
assertThat(tp.getSourceState()).isEqualTo("PENDING");
}
@Test
void shouldDetectStateFromGuardClauseSibling(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
private StateMachine sm;
public void processOrder(OrderState state) {
if (state != OrderState.PENDING) {
return;
}
sm.sendEvent(OrderEvent.PAY);
}
}
class StateMachine {
public void sendEvent(OrderEvent e) {}
}
enum OrderState { PENDING }
enum OrderEvent { PAY }
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
assertThat(triggers).hasSize(1);
TriggerPoint tp = triggers.get(0);
assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
assertThat(tp.getSourceState()).isEqualTo("PENDING");
}
@Test
void shouldDetectStateFromSwitchCaseSibling(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
private StateMachine sm;
public void processOrder(OrderState state) {
switch (state) {
case PENDING:
sm.sendEvent(OrderEvent.PAY);
break;
case PAID:
sm.sendEvent(OrderEvent.SHIP);
break;
}
}
}
class StateMachine {
public void sendEvent(OrderEvent e) {}
}
enum OrderState { PENDING, PAID }
enum OrderEvent { PAY, SHIP }
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
assertThat(triggers).hasSize(2);
TriggerPoint payTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.PAY")).findFirst().get();
assertThat(payTrigger.getSourceState()).isEqualTo("PENDING");
TriggerPoint shipTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.SHIP")).findFirst().get();
assertThat(shipTrigger.getSourceState()).isEqualTo("PAID");
}
}

View File

@@ -89,7 +89,8 @@ public class GenericEventDetectorTest {
"import org.springframework.statemachine.StateMachine;\n" +
"public class MyService {\n" +
" private StateMachine<String, String> stateMachine;\n" +
" public MyEnum getEvent() { return MyEnum.STATE_A; }\n" + // Pretend it returns the enum
" public MyEnum getEvent() { return someExternalCall(); }\n" +
" public MyEnum someExternalCall() { return null; }\n" +
" public void trigger() {\n" +
" stateMachine.sendEvent(this.getEvent());\n" +
" }\n" +

View File

@@ -12,9 +12,11 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
class CallGraphBuilderTest {
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineCoreTest {
@Test
void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException {
@@ -43,7 +45,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderService")
@@ -94,7 +96,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.PersisterService")
@@ -146,7 +148,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -200,7 +202,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -222,197 +224,6 @@ class CallGraphBuilderTest {
.containsExactlyInAnyOrder("OrderEvents.PAY");
}
@Test
void shouldResolvePolymorphicInheritanceAcrossChain() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
doProcessOrderEvent(domainEvent);
}
private void doProcessOrderEvent(RichOrderEvent domainEvent) {
OrderEvents eventType = assertSupportedOrderEvent(domainEvent.getType());
service.updateOrderState(eventType);
}
private OrderEvents assertSupportedOrderEvent(OrderEvents e) {
return e;
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {
// sendEvent
}
}
interface RichOrderEvent {
OrderEvents getType();
}
class CancelOrderEvent implements RichOrderEvent {
public OrderEvents getType() { return OrderEvents.CANCELLED; }
}
class ReceiveOrderEvent implements RichOrderEvent {
public OrderEvents getType() { return OrderEvents.RECEIVED; }
}
enum OrderEvents { CREATE, PAY, CANCELLED, RECEIVED }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_poly");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
.containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED");
}
@Test
void shouldUnwrapDeepMethodWrappers() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
OrderEvents eventType = wrap3(wrap2(wrap1(domainEvent.getType())));
service.updateOrderState(eventType);
}
private OrderEvents wrap1(OrderEvents e) { return e; }
private OrderEvents wrap2(OrderEvents e) { return e; }
private OrderEvents wrap3(OrderEvents e) { return e; }
}
class OrderService {
public void updateOrderState(OrderEvents event) { }
}
class RichOrderEvent {
public OrderEvents getType() { return OrderEvents.CREATE; }
}
enum OrderEvents { CREATE }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_deep_wrap");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
.containsExactlyInAnyOrder("OrderEvents.CREATE");
}
@Test
void shouldResolvePolymorphicInheritanceThroughDelegation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(BaseEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
interface BaseEvent {
OrderEvents getType();
}
abstract class AbstractEvent implements BaseEvent {
protected OrderEvents internalGetType() {
return OrderEvents.PAY;
}
}
class ConcreteEvent extends AbstractEvent {
public OrderEvents getType() {
return internalGetType();
}
}
class AnotherConcreteEvent implements BaseEvent {
public OrderEvents getType() {
return delegateToHelper();
}
private OrderEvents delegateToHelper() {
return OrderEvents.CANCELLED;
}
}
enum OrderEvents { PAY, CANCELLED }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_poly_delegation");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED");
}
@Test
void shouldHandleMissingImplementationsGracefully() throws IOException {
String source = """
@@ -438,7 +249,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -489,7 +300,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -510,64 +321,6 @@ class CallGraphBuilderTest {
.containsExactlyInAnyOrder("OrderEvents.RECEIVED");
}
@Test
void shouldResolveMethodInheritedFromAbstractBaseClass() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(BaseEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
interface BaseEvent {
OrderEvents getType();
}
abstract class AbstractEvent implements BaseEvent {
public OrderEvents getType() {
return OrderEvents.PAY;
}
}
// Concrete subclass inherits getType() but doesn't override it!
class InheritingEvent extends AbstractEvent {
}
enum OrderEvents { PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_inherited");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.PAY");
}
@Test
void shouldStopTracingRecursiveMethodsGracefully() throws IOException {
String source = """
@@ -597,7 +350,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -650,7 +403,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -695,7 +448,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -747,7 +500,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -764,9 +517,10 @@ class CallGraphBuilderTest {
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
// It successfully traces the local variable 'event' to its anonymous class initializer
assertThat(chain.getTriggerPoint().getEvent()).startsWith("new RichEvent(){");
assertThat(chain.getTriggerPoint().getEvent()).endsWith(".getType()");
// The engine traces the anonymous class's getType() through the RichEvent interface,
// resolving to OrderEvents.CREATE via the enum return-type fallback.
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.CREATE");
}
@Test
@@ -793,7 +547,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -837,7 +591,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -859,7 +613,7 @@ class CallGraphBuilderTest {
}
@Test
void shouldTraceLastTextualAssignmentInTryCatchBlocks() throws IOException {
void shouldTraceAllTextualAssignmentsInTryCatchBlocks() throws IOException {
String source = """
package com.example;
public class OrderController {
@@ -887,7 +641,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
@@ -904,126 +658,8 @@ class CallGraphBuilderTest {
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
// The ASTVisitor picks up Assignments in textual order.
// CANCELLED is the last one textually in the method.
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED");
}
@Test
void shouldTraceArrayAndCollectionAccessGracefully() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent() {
OrderEvents[] events = new OrderEvents[]{OrderEvents.PAY};
service.updateOrderState(events[0]);
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
enum OrderEvents { PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_array");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
// We don't dynamically resolve array indexes, but it shouldn't crash.
// It returns the array access expression as a string.
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("events[0]");
}
@Test
void shouldProperlyUnwrapDeeplyNestedWrapperCallsForFix1() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(MyEvent event) {
OrderEvents eventType = wrap1(wrap2(wrap3(event.getType())));
service.updateOrderState(eventType);
}
private OrderEvents wrap1(OrderEvents e) { return e; }
private OrderEvents wrap2(OrderEvents e) { return e; }
private OrderEvents wrap3(OrderEvents e) { return e; }
}
class OrderService {
public void updateOrderState(OrderEvents event) { }
}
class MyEvent {
public OrderEvents getType() { return OrderEvents.CREATE; }
}
enum OrderEvents { CREATE }
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_wrap");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("processOrderEvent").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("updateOrderState").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.CREATE");
}
@Test
void shouldNotExtractClassInstanceCreationEvenWithComplexArgsForFix2() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void createOrder(String oID) {
service.processOrderEvent(new OrderCancelledEvent(new OrderId(oID)));
}
}
class OrderService {
public void processOrderEvent(OrderCancelledEvent event) { }
}
class OrderCancelledEvent {
public OrderCancelledEvent(OrderId id) {}
}
class OrderId {
public OrderId(String id) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_complex_cic");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("createOrder").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("processOrderEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getEvent()).isEqualTo("new OrderCancelledEvent(new OrderId(oID))");
assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED");
}
@Test
@@ -1073,7 +709,7 @@ class CallGraphBuilderTest {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.controller.EventController").methodName("handleEvent").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.service.EventService").methodName("process").event("type").build();
@@ -1081,4 +717,290 @@ class CallGraphBuilderTest {
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B");
}
@Test
void shouldGracefullyHandleComplexMethodChainsWithoutCrashing() throws IOException {
String source = """
package com.example;
import java.util.Optional;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(Optional.of(domainEvent).get().getEvent());
}
}
class DomainEvent {
public String getEvent() { return "COMPLEX_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_complex_chain");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
}
@Test
void shouldResolveConstantsFromStringConcatenation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getCustomEvent());
}
}
class DomainEvent {
public static final String PREFIX = "ORDER_";
public String getCustomEvent() {
return PREFIX + "CREATED";
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_concat");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_CREATED");
}
@Test
void shouldResolveConstantsFromStringToString() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getEventString());
}
}
class DomainEvent {
public static final String ACTUAL_STRING = "STRING_LITERAL_EVENT";
public String getEventString() {
return ACTUAL_STRING.toString();
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_string_tostring");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("STRING_LITERAL_EVENT");
}
@Test
void shouldResolveEventFromLambda() throws IOException {
String source = """
package com.example;
import java.util.List;
public class OrderController {
private OrderService service;
public void handleList(List<LambdaEvent> events) {
events.forEach(e -> service.process(e.getType()));
}
}
class LambdaEvent {
private String type;
public LambdaEvent(String typeArg) { this.type = typeArg; }
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_lambda");
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("handleList").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
System.out.println("DEBUG LAMBDA CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents()));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).isNullOrEmpty();
}
@Test
void shouldResolveEventFromAnonymousClass() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new EventProvider() {
@Override
public String getEvent() { return "ANONYMOUS_EVENT"; }
}.getEvent());
}
}
interface EventProvider {
String getEvent();
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_anonymous");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ANONYMOUS_EVENT");
}
@Test
void shouldResolveEventFromBranchingAssignments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(boolean isRefund) {
String event;
if (isRefund) {
event = "REFUND_EVENT";
} else {
event = "CHARGE_EVENT";
}
service.process(event);
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_branch");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("REFUND_EVENT", "CHARGE_EVENT");
}
@Test
void shouldResolveEventFromMethodReference() throws IOException {
String source = """
package com.example;
import java.util.List;
public class OrderController {
private OrderService service;
public void handleMethodRef() {
List<RefEvent> events = List.of(new RefEvent("METHOD_REF_EVENT"));
events.forEach(this::delegateProcessing);
}
private void delegateProcessing(RefEvent e) {
service.process(e.getType());
}
}
class OrderService {
public void process(String event) {}
}
class RefEvent {
private String type;
public RefEvent(String type) { this.type = type; }
public String getType() { return type; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_method_ref");
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("handleMethodRef").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("METHOD_REF_EVENT");
}
@Test
void shouldResolveEventFromJavaRecord() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleRecord() {
RecordEvent e = new RecordEvent("RECORD_EVENT", 42);
service.process(e.type());
}
}
class OrderService {
public void process(String event) {}
}
record RecordEvent(String type, int someId) {}
""";
Path tempDir = Files.createTempDirectory("callgraph_record");
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("handleRecord").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("RECORD_EVENT");
}
}

View File

@@ -0,0 +1,315 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineExtendedTest {
/**
* Diagnostic: Generics, inherited processing, multi-enum constructor args,
* and internal constructor mapping via a switch method.
* <p>
* Pattern:
* 1. Abstract base class AbstractEventClass<T> holds the payload and transitionType.
* 2. Abstract base class also contains the method that actually fires the state machine.
* 3. MyEventClass<T> constructor takes multiple enums (InputEnum1, InputEnum2).
* 4. MyEventClass assigns: this.transitionType = computeTransitionType(InputEnum2).
* 5. computeTransitionType uses a switch to map InputEnum2 to TransitionEnum.
* <p>
* Root cause targeted: The engine must not eagerly grab InputEnum1.IGNORE or
* InputEnum2.SOURCE_B. It must evaluate the internal computeTransitionType()
* method during instantiation to resolve the final TransitionEnum.STATE_Y.
*/
@Test
void shouldResolveTransitionEnumThroughGenericsAndInternalConstructorSwitchComputation() throws IOException {
String source = """
package com.example;
public class SystemController {
private StateMachine machine;
public void processIncomingPayload(String rawData) {
// Creates concrete class with multiple enums and a generic payload.
// Engine MUST NOT stop at IGNORE or SOURCE_B.
MyEventClass<String> event = new MyEventClass<>(
rawData,
InputEnum1.IGNORE,
InputEnum2.SOURCE_B
);
// Calls the inherited method that actually processes the event
event.fireEvent(machine);
}
}
abstract class AbstractEventClass<T> {
protected T payload;
protected TransitionEnum transitionType;
public AbstractEventClass(T payload) {
this.payload = payload;
}
// The method to actually process events
public void fireEvent(StateMachine machine) {
machine.fire(this.getTransitionType());
}
public TransitionEnum getTransitionType() {
return this.transitionType;
}
}
class MyEventClass<T> extends AbstractEventClass<T> {
private InputEnum1 routingFlag;
public MyEventClass(T payload, InputEnum1 flag, InputEnum2 source) {
super(payload);
this.routingFlag = flag;
// The trap: Assignment via an internal method evaluation
this.transitionType = computeTransitionType(source);
}
private TransitionEnum computeTransitionType(InputEnum2 source) {
return switch (source) {
case SOURCE_A -> TransitionEnum.STATE_X;
case SOURCE_B -> TransitionEnum.STATE_Y; // Expected resolution
case SOURCE_C -> TransitionEnum.STATE_Z;
};
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum InputEnum1 { IGNORE, PROCESS_ASYNC }
enum InputEnum2 { SOURCE_A, SOURCE_B, SOURCE_C }
enum TransitionEnum { STATE_X, STATE_Y, STATE_Z }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_generics_computation");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.SystemController")
.methodName("processIncomingPayload")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("GENERICS & COMPUTATION polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// The assertion to prove the bug is fixed.
// If the engine returns InputEnum2.SOURCE_B or InputEnum1.IGNORE, it fails.
// It MUST evaluate computeTransitionType() and return TransitionEnum.STATE_Y.
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("TransitionEnum.STATE_Y");
}
/**
* Diagnostic: Enum tracked through an overridden method and a super() call.
*
* Pattern:
* 1. EntryPoint calls an overridden method on a subclass (CustomHandler.handleEvent)
* with an initial enum (ActionType.BEGIN_CHECKOUT).
* 2. The subclass intercepts the call, does some localized logic, and delegates
* back to the parent using super.handleEvent(ActionType).
* 3. The parent class (BaseHandler) takes that enum, transforms it via a switch
* statement to a TransitionEnum, and fires the state machine.
*
* Root cause targeted: The engine must successfully resolve the `super` keyword
* to the parent class's AST node, maintain the argument mapping across the inheritance
* boundary, and evaluate the switch to get the final TransitionEnum. It must not
* get stuck in a recursive loop or eagerly return ActionType.BEGIN_CHECKOUT.
*/
@Test
void shouldResolveMappedEnumAcrossOverriddenMethodAndSuperDelegation() throws IOException {
String source = """
package com.example;
public class ApiEndpoint {
private CustomHandler handler;
public void triggerCheckout() {
// Entry point passes the initial enum
handler.handleEvent(ActionType.BEGIN_CHECKOUT);
}
}
abstract class BaseHandler {
protected StateMachine machine;
// The method that gets overridden, but ultimately does the work
public void handleEvent(ActionType action) {
TransitionEnum transition = switch (action) {
case BEGIN_CHECKOUT -> TransitionEnum.CHECKOUT_STARTED; // Expected
case ABORT_CHECKOUT -> TransitionEnum.CHECKOUT_CANCELLED;
};
machine.fire(transition);
}
}
class CustomHandler extends BaseHandler {
@Override
public void handleEvent(ActionType action) {
// Custom logic before delegation (simulated)
if (action == null) return;
// The trap: The engine must follow 'super' to BaseHandler
// and carry the 'action' argument with it.
super.handleEvent(action);
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum ActionType { BEGIN_CHECKOUT, ABORT_CHECKOUT }
enum TransitionEnum { CHECKOUT_STARTED, CHECKOUT_CANCELLED }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_super_override");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.ApiEndpoint")
.methodName("triggerCheckout")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("SUPER DELEGATION polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// The assertion to prove the bug is fixed.
// It MUST NOT contain "ActionType.BEGIN_CHECKOUT".
// It MUST successfully trace through super.handleEvent() and resolve the switch.
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("TransitionEnum.CHECKOUT_STARTED");
}
@Test
void shouldResolveCorrectEnumWhenNamesCollide() throws IOException {
String source = """
package com.example;
public class OrderService {
private StateMachine machine;
public void processOrder() {
// We use OrderEvent.PROCESS
machine.fire(OrderEvent.PROCESS);
}
}
public class InventoryService {
private StateMachine machine;
public void updateInventory() {
// We use InternalEvent.PROCESS which has same simple name
machine.fireInternal(InternalEvent.PROCESS);
}
public void wrongProcess() {
// This calls fire(OrderEvent) but with InternalEvent.PROCESS
// It should be REJECTED by type-aware heuristic matching if it matches fire(OrderEvent)
machine.fire(InternalEvent.PROCESS);
}
}
class StateMachine {
// Two overloaded methods or methods in different classes (simulated by names)
public void fire(OrderEvent event) {}
public void fireInternal(InternalEvent event) {}
}
enum OrderEvent { PROCESS, CANCEL }
enum InternalEvent { PROCESS, AUDIT }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_collision");
java.nio.file.Files.writeString(tempDir.resolve("CollisionConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
// Scenario 1: Trace OrderService.processOrder
EntryPoint entryPoint1 = EntryPoint.builder()
.className("com.example.OrderService")
.methodName("processOrder")
.build();
TriggerPoint trigger1 = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains1 = engine.findChains(List.of(entryPoint1), List.of(trigger1));
assertThat(chains1).hasSize(1);
assertThat(chains1.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactly("OrderEvent.PROCESS")
.doesNotContain("InternalEvent.PROCESS");
// Scenario 2: Trace InventoryService.updateInventory
EntryPoint entryPoint2 = EntryPoint.builder()
.className("com.example.InventoryService")
.methodName("updateInventory")
.build();
TriggerPoint trigger2 = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fireInternal")
.event("event")
.build();
List<CallChain> chains2 = engine.findChains(List.of(entryPoint2), List.of(trigger2));
assertThat(chains2).hasSize(1);
assertThat(chains2.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactly("InternalEvent.PROCESS")
.doesNotContain("OrderEvent.PROCESS");
}
}

View File

@@ -0,0 +1,957 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineGetterTest {
@Test
void shouldResolveConstantsFromAlternativeGetterName() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.get2Type());
}
}
class DomainEvent {
public String get2Type() {
return "ALTERNATIVE_EVENT";
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_alt_getter");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ALTERNATIVE_EVENT");
}
@Test
void shouldResolveConstantsFromInlineInstantiation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent().getEvent());
}
}
class DomainEvent {
public String getEvent() { return "INLINE_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithArguments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent("PAYLOAD_ID").getEvent());
}
}
class DomainEvent {
private String id;
public DomainEvent(String id) { this.id = id; }
public String getEvent() { return "INLINE_EVENT_WITH_ARGS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_args");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_ARGS");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithGenerics() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent<String>().getEvent());
}
}
class DomainEvent<T> {
public String getEvent() { return "INLINE_EVENT_WITH_GENERICS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_gen");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_GENERICS");
}
@Test
void shouldResolveConstantsFromInlineInstantiationInheritedMethod() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new SpecificDomainEvent().getEvent());
}
}
abstract class BaseDomainEvent {
public String getEvent() { return "BASE_EVENT_RETURN"; }
}
class SpecificDomainEvent extends BaseDomainEvent {
// no getEvent here, it inherits it!
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_hier");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithComplexArguments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(Order o, String abc) {
service.process(new DomainEvent(o.getId(), abc).getEvent());
}
}
class Order {
public String getId() { return "123"; }
}
class DomainEvent {
private String id;
private String extra;
public DomainEvent(String id, String extra) { this.id = id; this.extra = extra; }
public String getEvent() { return "INLINE_EVENT_COMPLEX_ARGS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_complex_args");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_COMPLEX_ARGS");
}
@Test
void shouldNotIncludeSubclassesForInlineInstantiation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new BaseEvent().getEvent());
}
}
class BaseEvent {
public String getEvent() { return "BASE_EVENT_RETURN"; }
}
class SubEvent extends BaseEvent {
public String getEvent() { return "SUB_EVENT_RETURN"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_subclass");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
// Only BASE_EVENT_RETURN should be found. SUB_EVENT_RETURN is a subclass and should not be included for direct instantiations
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
}
@Test
void shouldResolveConstantsFromFieldAssignedInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderPickedUpEvent().getType());
}
}
class OrderPickedUpEvent {
private String type;
public OrderPickedUpEvent() {
this.type = "PICKED_UP_IN_CONSTRUCTOR";
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_const");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("PICKED_UP_IN_CONSTRUCTOR");
}
@Test
void shouldResolveConstantsFromFieldAssignedInSuperConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderCancelledEvent().getType());
}
}
abstract class BaseEvent {
private String type;
public BaseEvent(String type, String otherVar) {
this.type = type;
}
public String getType() { return type; }
}
class OrderCancelledEvent extends BaseEvent {
public OrderCancelledEvent() {
super("CANCELLED_IN_SUPER", "someVar");
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_super_const");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_IN_SUPER");
}
@Test
void shouldResolveConstantsFromFieldAssignedToAnotherConstantInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new EventWithIndirectConstant().getType());
}
}
class EventConstants {
public static final String INDIRECT_EVENT = "INDIRECT_EVENT_VALUE";
}
class EventWithIndirectConstant {
private String type;
public EventWithIndirectConstant() {
this.type = EventConstants.INDIRECT_EVENT;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_indirect_const");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("INDIRECT_EVENT_VALUE");
}
@Test
void shouldResolveConstantsFromConstructorInvocation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new EventWithThisCall().getType());
}
}
class EventWithThisCall {
private String type;
public EventWithThisCall() {
this("THIS_CALL_CONSTANT");
}
public EventWithThisCall(String type) {
this.type = type;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_this_const");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("THIS_CALL_CONSTANT");
}
@Test
void shouldResolveConstantsFromDynamicMethodAssignmentInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderCancelledEvent().getType());
}
}
class OrderCancelledEvent {
private String cancelEventType;
public OrderCancelledEvent() {
this.cancelEventType = assertMatchingCancelEventType("some_sender");
}
private String assertMatchingCancelEventType(String sender) {
return "CANCELLED_BY_SENDER";
}
public String getType() { return cancelEventType; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_dynamic_rhs");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_BY_SENDER");
}
@Test
void shouldResolveConstantsFromFieldDeclarationsAndInitializers() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus1() {
service.process(new EventWithFieldDecl().getType());
}
public void handleStatus2() {
service.process(new EventWithInitBlock().getType());
}
}
class EventWithFieldDecl {
private String event = "FIELD_DECL";
public String getType() { return event; }
}
class EventWithInitBlock {
private String event;
{
event = "INIT_BLOCK";
}
public String getType() { return event; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_init");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build();
EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(ep1, ep2), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
java.util.List<String> allEvents = new java.util.ArrayList<>();
chains.forEach(c -> allEvents.addAll(c.getTriggerPoint().getPolymorphicEvents()));
org.assertj.core.api.Assertions.assertThat(allEvents).containsExactlyInAnyOrder("FIELD_DECL", "INIT_BLOCK");
}
@Test
void shouldFallbackToScraperForComplexInlineInstantiations() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
// The AST tracer will trace EventWithConstructorArg.getType()
// It will find `this.type = typeArg`, which it cannot resolve because it's a parameter.
// The fallback scraper should catch "COMPLEX_INLINE_CONST".
service.process(new EventWithConstructorArg("COMPLEX_INLINE_CONST", 123).getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_scraper");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
}
@Test
void shouldResolveEventFromConstructorAssignment() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
service.process(e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_constructor_assignment");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_CONSTRUCTOR");
}
@Test
void shouldPrioritizeSetterOverConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
e.setType("EVENT_FROM_SETTER");
service.process(e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public void setType(String t) { this.type = t; }
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_setter_priority");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_SETTER");
}
@Test
void shouldResolveEventFromFieldConstructorAssignment() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
private EventWithConstructorArg e;
public OrderController() {
this.e = new EventWithConstructorArg("EVENT_FROM_FIELD", 123);
}
public void handleStatus() {
service.process(this.e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_assignment");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
System.out.println("DEBUG CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents()));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_FIELD");
}
@Test
void shouldResolveEventFromLombokDataAndAllArgsConstructor() throws IOException {
String source = """
package com.example;
import lombok.Data;
import lombok.AllArgsConstructor;
public class OrderController {
private OrderService service;
public void handleLombok() {
LombokEvent e = new LombokEvent("LOMBOK_EVENT", 1);
service.process(e.getEvent());
}
}
class OrderService {
public void process(String event) {}
}
@Data
@AllArgsConstructor
class LombokEvent {
private String event;
private int id;
}
""";
Path tempDir = Files.createTempDirectory("callgraph_lombok");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("LOMBOK_EVENT");
}
@Test
void shouldFailHeuristicAndRequireAccurateLombokMapping() throws IOException {
String source = """
package com.example;
import lombok.Data;
import lombok.AllArgsConstructor;
public class OrderController {
private OrderService service;
public void handleLombok() {
MultiEvent e = new MultiEvent("CORRECT_EVENT", "WRONG_EVENT");
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
@Data
@AllArgsConstructor
class MultiEvent {
private String eventType;
private String unrelatedString;
}
""";
Path tempDir = Files.createTempDirectory("callgraph_lombok_heuristic_break");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactly("CORRECT_EVENT"); // If heuristic rips both, it will fail because it contains WRONG_EVENT too!
}
@Test
void shouldResolveEventsWhenFieldTransformerMethodMatchesSetterPatternButArgIsNonConstant() throws IOException {
String source = """
package com.example;
public class OrderController {
private Transformer transformer;
public void processEvent(String dto) {
Event e = transformer.transform(dto);
machine.fire(e.getType());
}
}
interface Transformer {
Event transform(String input);
}
class TransformerImpl implements Transformer {
public Event transform(String input) {
return new Event("ORDER_DISPATCHED");
}
}
class Event {
private String type;
public Event(String type) { this.type = type; }
public String getType() { return type; }
}
class Machine {
public void fire(String event) {}
}
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_transformer");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.Machine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
// Must resolve through TransformerImpl.transform() → "ORDER_DISPATCHED",
// NOT return empty polymorphicEvents due to early exit on the setter match.
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("ORDER_DISPATCHED");
}
@Test
void shouldResolveEnumPassedThroughWrapperObjectAndGetter() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderMapper mapper;
private StateMachine machine;
public void processOrder(String dto) {
OrderEvent event = mapper.toEvent(dto);
machine.fire(event.getType());
}
}
interface OrderMapper {
OrderEvent toEvent(String input);
}
class OrderMapperImpl implements OrderMapper {
public OrderEvent toEvent(String input) {
return new OrderEvent(States.PAYMENT_RECEIVED);
}
}
class OrderEvent {
private States type;
public OrderEvent(States type) { this.type = type; }
public States getType() { return type; } // bare field, no this.
}
class StateMachine {
public void fire(States event) {}
}
enum States { PAYMENT_RECEIVED, CANCELLED }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_wrapper");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrder")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("DIAGNOSTIC polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("States.PAYMENT_RECEIVED");
}
@Test
void shouldResolveTransformingGetterThatMapsDtoEnumToStateMachineEnum() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderMapper mapper;
private StateMachine machine;
public void processOrder(String dto) {
OrderMsg msg = mapper.toMsg(dto);
machine.fire(msg.getSmEvent());
}
}
interface OrderMapper {
OrderMsg toMsg(String input);
}
class OrderMapperImpl implements OrderMapper {
public OrderMsg toMsg(String input) {
return new OrderMsg(OrderType.PAY);
}
}
class OrderMsg {
private OrderType type;
public OrderMsg(OrderType type) { this.type = type; }
public SMEvent getSmEvent() {
return switch (type) {
case PAY -> SMEvent.PAY_RECEIVED;
case CANCEL -> SMEvent.ORDER_CANCELLED;
};
}
}
class StateMachine {
public void fire(SMEvent event) {}
}
enum OrderType { PAY, CANCEL }
enum SMEvent { PAY_RECEIVED, ORDER_CANCELLED }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_twoenum");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrder")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("TWO-ENUM polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// Must return the SM enum, NOT the DTO enum (OrderType.PAY would be wrong)
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED");
}
}

View File

@@ -0,0 +1,409 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEnginePolymorphicTest {
@Test
void shouldResolvePolymorphicInheritanceAcrossChain() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
doProcessOrderEvent(domainEvent);
}
private void doProcessOrderEvent(RichOrderEvent domainEvent) {
OrderEvents eventType = assertSupportedOrderEvent(domainEvent.getType());
service.updateOrderState(eventType);
}
private OrderEvents assertSupportedOrderEvent(OrderEvents e) {
return e;
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {
// sendEvent
}
}
interface RichOrderEvent {
OrderEvents getType();
}
class CancelOrderEvent implements RichOrderEvent {
public OrderEvents getType() { return OrderEvents.CANCELLED; }
}
class ReceiveOrderEvent implements RichOrderEvent {
public OrderEvents getType() { return OrderEvents.RECEIVED; }
}
enum OrderEvents { CREATE, PAY, CANCELLED, RECEIVED }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_poly");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
.containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED");
}
@Test
void shouldResolveTernaryAndStringPolymorphicReturns() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent() {
service.updateOrderState(new MysteryPayload().getType());
}
}
class OrderService {
public void updateOrderState(Object event) {
// sendEvent
}
}
class MysteryPayload {
public Object getType() {
int a = 5;
if (a > 10) {
return "RAW_STRING_EVENT";
}
return a > 5 ? OrderEvents.ABCD : OrderEvents.PAY;
}
}
enum OrderEvents { ABCD, PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_ternary_string");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
.containsExactlyInAnyOrder("OrderEvents.ABCD", "OrderEvents.PAY", "RAW_STRING_EVENT");
}
@Test
void shouldResolvePolymorphicInheritanceThroughDelegation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(BaseEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
interface BaseEvent {
OrderEvents getType();
}
abstract class AbstractEvent implements BaseEvent {
protected OrderEvents internalGetType() {
return OrderEvents.PAY;
}
}
class ConcreteEvent extends AbstractEvent {
public OrderEvents getType() {
return internalGetType();
}
}
class AnotherConcreteEvent implements BaseEvent {
public OrderEvents getType() {
return delegateToHelper();
}
private OrderEvents delegateToHelper() {
return OrderEvents.CANCELLED;
}
}
enum OrderEvents { PAY, CANCELLED }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_poly_delegation");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED");
}
@Test
void shouldResolveMethodInheritedFromAbstractBaseClass() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(BaseEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
interface BaseEvent {
OrderEvents getType();
}
abstract class AbstractEvent implements BaseEvent {
public OrderEvents getType() {
return OrderEvents.PAY;
}
}
// Concrete subclass inherits getType() but doesn't override it!
class InheritingEvent extends AbstractEvent {
}
enum OrderEvents { PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_inherited");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.PAY");
}
@Test
void shouldResolveConstantsFromAbstractSuperclass() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new PostAcceptanceOrderModificationsRejectedEvent().getType());
}
}
abstract class AbstractPostAcceptanceOrderModificationsEvent {
private String event;
public AbstractPostAcceptanceOrderModificationsEvent(String event) {
this.event = event;
}
public String getType() { return event; }
}
class PostAcceptanceOrderModificationsRejectedEvent extends AbstractPostAcceptanceOrderModificationsEvent {
public PostAcceptanceOrderModificationsRejectedEvent() {
super("POST_ACCEPTANCE_REJECTED");
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_post_acc");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("POST_ACCEPTANCE_REJECTED");
}
@Test
void shouldResolveMappedTransitionEnumThroughAbstractBaseClassAndMultipleArgs() throws IOException {
String source = """
package com.example;
public class FlowController {
private CommandFactory factory;
private StateMachine machine;
public void handleRequest(String payload) {
// Creates subclass with multiple args, including intermediate enum
BaseCommand cmd = factory.buildCommand(payload);
// Fires using the mapped enum from the inherited base method
machine.fire(cmd.getTransitionEvent());
}
}
class CommandFactory {
public BaseCommand buildCommand(String payload) {
// Passing multiple args: String, Enum1, String, int
return new WebCommand(payload, SourceType.WEB_API, "meta_v1", 42);
}
}
abstract class BaseCommand {
protected String id;
protected SourceType sourceType; // The intermediate enum
protected String metadata;
public BaseCommand(String id, SourceType sourceType, String metadata) {
this.id = id;
this.sourceType = sourceType;
this.metadata = metadata;
}
// The tricky part: Inherited method doing the mapping
public TransitionEnum getTransitionEvent() {
return switch (this.sourceType) {
case WEB_API -> TransitionEnum.ON_WEB_START;
case MOBILE_APP -> TransitionEnum.ON_MOBILE_START;
case CRON_JOB -> TransitionEnum.ON_SYSTEM_START;
};
}
}
class WebCommand extends BaseCommand {
private int priorityLevel;
public WebCommand(String id, SourceType sourceType, String metadata, int priorityLevel) {
super(id, sourceType, metadata); // Engine must track through super()
this.priorityLevel = priorityLevel;
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum SourceType { WEB_API, MOBILE_APP, CRON_JOB }
enum TransitionEnum { ON_WEB_START, ON_MOBILE_START, ON_SYSTEM_START }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_complex_inheritance");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.FlowController")
.methodName("handleRequest")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("COMPLEX INHERITANCE polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// The assertion to prove the bug is fixed.
// It MUST NOT contain "SourceType.WEB_API"
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("TransitionEnum.ON_WEB_START");
}
}

View File

@@ -0,0 +1,472 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineTypeTest {
@Test
void shouldResolveLocalSetterAndSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
MysteryPayload a = new MysteryPayload();
a.setType(my_func(vv));
sm.sendEvent(a.getType());
}
private OrderEvents my_func(String q) {
return switch (q) {
case "a" -> OrderEvents.A8;
case "b" -> OrderEvents.A133;
default -> throw new IllegalArgumentException("Unknown");
};
}
}
enum OrderEvents { A8, A133 }
class MysteryPayload {
private OrderEvents type;
public void setType(OrderEvents type) { this.type = type; }
public OrderEvents getType() { return type; }
}
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
System.out.println("Resolved poly events: " + chain.getTriggerPoint().getPolymorphicEvents());
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133");
}
@Test
void shouldResolveOldStyleSwitchStatementPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventFromOldSwitch(vv));
}
private OrderEvents getEventFromOldSwitch(String q) {
switch (q) {
case "a": return OrderEvents.A8;
case "b":
case "c": return OrderEvents.A133;
default: return OrderEvents.PAY;
}
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@Test
void shouldResolveMultiValueSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventMultiValue(vv));
}
private OrderEvents getEventMultiValue(String q) {
return switch (q) {
case "a", "b" -> OrderEvents.A8;
case "c" -> OrderEvents.A133;
default -> OrderEvents.PAY;
};
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@Test
void shouldResolveSwitchExpressionWithBlockAndYieldPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventWithYield(vv));
}
private OrderEvents getEventWithYield(String q) {
return switch (q) {
case "a" -> {
System.out.println("Processing a");
yield OrderEvents.A8;
}
case "b" -> { yield OrderEvents.A133; }
default -> OrderEvents.PAY;
};
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@Test
void shouldTraceEventsThroughMapStructInterfaceImpl() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
private OrderMapper mapper = new OrderMapperImpl();
public void processOrderEvent(String eventDtoStr) {
OrderEvent e = mapper.toEvent(eventDtoStr);
service.updateOrderState(e.getEvent());
}
}
class OrderService {
public void updateOrderState(String event) {}
}
interface OrderMapper {
OrderEvent toEvent(String source);
}
class OrderMapperImpl implements OrderMapper {
@Override
public OrderEvent toEvent(String source) {
return new OrderEvent("MAPPED_EVENT");
}
}
class OrderEvent {
private String event;
public OrderEvent(String event) { this.event = event; }
public String getEvent() { return event; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_test_mapstruct");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("MAPPED_EVENT");
}
@Test
void shouldTraceArrayAndCollectionAccessGracefully() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent() {
OrderEvents[] events = new OrderEvents[]{OrderEvents.PAY};
service.updateOrderState(events[0]);
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
enum OrderEvents { PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_array");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
// We don't dynamically resolve array indexes, but it shouldn't crash.
// It returns the array access expression as a string.
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("new OrderEvents[]{OrderEvents.PAY}[0]");
}
@Test
void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getEventForStatus());
}
}
class DomainEvent {
private OrderStatus status;
public String getEventForStatus() {
switch (status) {
case NEW: return "CREATE_EVENT";
case PAID: return "PAY_EVENT";
default: return "CANCEL_EVENT";
}
}
}
class OrderService {
public void process(String event) {}
}
enum OrderStatus { NEW, PAID }
""";
Path tempDir = Files.createTempDirectory("callgraph_switch");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("CREATE_EVENT", "PAY_EVENT", "CANCEL_EVENT");
}
@Test
void shouldResolveConstantsFromEnumReturn() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getEventEnum());
}
}
class DomainEvent {
public String getEventEnum() {
return MyEvents.ORDER_PAID.name();
}
}
class OrderService {
public void process(String event) {}
}
enum MyEvents { ORDER_PAID }
""";
Path tempDir = Files.createTempDirectory("callgraph_enum_return");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_PAID");
}
@Test
void shouldResolveEventFromArrayAndList() throws IOException {
String source = """
package com.example;
import java.util.List;
public class OrderController {
private OrderService service;
public void handleArray() {
String[] events = {"ARRAY_EVENT"};
service.process(events[0]);
}
public void handleList() {
List<String> listEvents = List.of("LIST_EVENT");
service.process(listEvents.get(0));
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_array");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleArray").build();
EntryPoint entryPoint2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleList").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint1, entryPoint2), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("ARRAY_EVENT", "LIST_EVENT");
}
@Test
void shouldResolveEnumValueOfFromString(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class Endpoint {
private OrderService service;
public void trigger(String eventString) {
service.doSomething(eventString);
}
}
class OrderService {
public void doSomething(String str) {
MyEvents event = mapToEnum(str);
sendEvent(event);
}
private MyEvents mapToEnum(String str) {
return MyEvents.valueOf(str);
}
public void sendEvent(MyEvents e) {}
}
enum MyEvents { A, B }
""";
Files.writeString(tempDir.resolve("Endpoint.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.Endpoint")
.methodName("trigger")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("sendEvent")
.event("e")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("MyEvents.A", "MyEvents.B");
}
}

View File

@@ -0,0 +1,271 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineWrapperTest {
@Test
void shouldUnwrapDeepMethodWrappers() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
OrderEvents eventType = wrap3(wrap2(wrap1(domainEvent.getType())));
service.updateOrderState(eventType);
}
private OrderEvents wrap1(OrderEvents e) { return e; }
private OrderEvents wrap2(OrderEvents e) { return e; }
private OrderEvents wrap3(OrderEvents e) { return e; }
}
class OrderService {
public void updateOrderState(OrderEvents event) { }
}
class RichOrderEvent {
public OrderEvents getType() { return OrderEvents.CREATE; }
}
enum OrderEvents { CREATE }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_deep_wrap");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
.containsExactlyInAnyOrder("OrderEvents.CREATE");
}
@Test
void shouldProperlyUnwrapDeeplyNestedWrapperCallsForFix1() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(MyEvent event) {
OrderEvents eventType = wrap1(wrap2(wrap3(event.getType())));
service.updateOrderState(eventType);
}
private OrderEvents wrap1(OrderEvents e) { return e; }
private OrderEvents wrap2(OrderEvents e) { return e; }
private OrderEvents wrap3(OrderEvents e) { return e; }
}
class OrderService {
public void updateOrderState(OrderEvents event) { }
}
class MyEvent {
public OrderEvents getType() { return OrderEvents.CREATE; }
}
enum OrderEvents { CREATE }
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_wrap");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("processOrderEvent").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("updateOrderState").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.CREATE");
}
@Test
void shouldNotExtractClassInstanceCreationEvenWithComplexArgsForFix2() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void createOrder(String oID) {
service.processOrderEvent(new OrderCancelledEvent(new OrderId(oID)));
}
}
class OrderService {
public void processOrderEvent(OrderCancelledEvent event) { }
}
class OrderCancelledEvent {
public OrderCancelledEvent(OrderId id) {}
}
class OrderId {
public OrderId(String id) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_complex_cic");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("createOrder").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("processOrderEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getEvent()).isEqualTo("new OrderCancelledEvent(new OrderId(oID))");
}
@Test
void shouldNotCrashOnStaticMethodWrapping() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(String.valueOf(domainEvent.getEvent()));
}
}
class DomainEvent {
public String getEvent() { return "STATIC_WRAP_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_static_wrap");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
}
@Test
void shouldNotExtractConstantsFromNestedClassInstanceCreations() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleNestedConstructors() {
// The event contains a nested constructor. The heuristic should NOT extract WRONG_EVENT.
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload("WRONG_EVENT"));
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
class OuterEvent {
private String eventType;
private InnerPayload payload;
public OuterEvent(String eventType, InnerPayload payload) {
this.eventType = eventType;
this.payload = payload;
}
public String getEventType() { return eventType; }
}
class InnerPayload {
private String someData;
public InnerPayload(String someData) { this.someData = someData; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_constructor");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleNestedConstructors").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactly("CORRECT_EVENT");
}
@Test
void shouldNotExtractConstantsFromNestedArraysOfObjects() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleNestedArrays() {
// The event contains a nested array of objects. The heuristic should NOT extract WRONG_EVENT.
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload[] { new InnerPayload("WRONG_EVENT") });
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
class OuterEvent {
private String eventType;
private InnerPayload[] payloads;
public OuterEvent(String eventType, InnerPayload[] payloads) {
this.eventType = eventType;
this.payloads = payloads;
}
public String getEventType() { return eventType; }
}
class InnerPayload {
private String someData;
public InnerPayload(String someData) { this.someData = someData; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_array");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleNestedArrays").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactly("CORRECT_EVENT");
}
}

View File

@@ -0,0 +1,71 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class InheritedMethodBugTest {
@Test
void shouldResolveEventFromInheritedProtectedMethodWithoutThis() throws IOException {
String source = """
package com.example;
public class ChildController extends ParentController {
private StateMachine machine;
public void process() {
// Calling inherited protected method WITHOUT 'this.' or 'super.'
machine.fire(getInheritedEvent());
}
}
abstract class ParentController {
protected TransitionEnum getInheritedEvent() {
return TransitionEnum.STATE_P; // Protected method returns STATE_P
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum TransitionEnum { STATE_P, STATE_Q }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_inherited");
Files.writeString(tempDir.resolve("Config.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.ChildController")
.methodName("process")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("Poly events from inherited method: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
// It SHOULD resolve to STATE_P by finding getInheritedEvent() in ParentController
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_P");
}
}

View File

@@ -0,0 +1,285 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class JdtCallGraphEngineIntegrationTest {
private CodebaseContext context;
private SpringBeanRegistry registry;
private JdtCallGraphEngine engine;
@BeforeEach
void setUp() throws IOException {
Path projectRoot = Path.of("../state_machines/extended_analysis_sample").toAbsolutePath().normalize();
context = new CodebaseContext();
context.setProjectRoot(projectRoot);
context.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> cp = resolver.resolveClasspath(projectRoot);
if (!cp.isEmpty()) {
context.setClasspath(cp);
}
context.setResolveBindings(true);
context.scan(Set.of(projectRoot), Collections.emptySet());
registry = new SpringBeanRegistry();
SpringContextScanner scanner = new SpringContextScanner(registry);
for (var cu : context.getCompilationUnits()) {
cu.accept(scanner);
}
SpringDependencyResolver dependencyResolver = new SpringDependencyResolver(registry);
InjectionPointAnalyzer injectionAnalyzer = new InjectionPointAnalyzer(dependencyResolver);
engine = new JdtCallGraphEngine(context, injectionAnalyzer);
}
@Test
void shouldResolvePrimaryBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.QuirkController")
.methodName("testPrimary")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.PrimaryQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary",
"click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk"
);
}
@Test
void shouldResolveQualifierBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.QuirkController")
.methodName("testQualifier")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.QualifierQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier",
"click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk"
);
}
@Test
void shouldResolveNamedBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.QuirkController")
.methodName("testNamed")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.NamedQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.QuirkController.testNamed",
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
);
}
@Test
void shouldResolveFallbackBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.QuirkController")
.methodName("testFallback")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.PrimaryQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.QuirkController.testFallback",
"click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk"
);
}
@Test
void shouldResolveBeanParameterInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.config.BeanParameterTestConfig")
.methodName("myBeanParamTester")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.NamedQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.config.BeanParameterTestConfig.myBeanParamTester",
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
);
}
@Test
void shouldResolveOrderedBeanInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.OrderedTestController")
.methodName("testOrder")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.HighPriorityOrderedService")
.methodName("doAction")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain())
.containsExactly(
"click.kamil.examples.statemachine.extended.web.OrderedTestController.testOrder",
"click.kamil.examples.statemachine.extended.service.HighPriorityOrderedService.doAction"
);
}
@Test
void shouldResolveFieldInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.FieldInjectionController")
.methodName("testField")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.NamedQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain()).contains(
"click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField",
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
);
}
@Test
void shouldResolveSetterInjection() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.SetterInjectionController")
.methodName("testSetter")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.NamedQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain()).contains(
"click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter",
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
);
}
@Test
void shouldResolveAbstractInheritedBean() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.config.AbstractBaseConfig")
.methodName("inheritedBeanTester")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.NamedQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain()).contains(
"click.kamil.examples.statemachine.extended.config.AbstractBaseConfig.inheritedBeanTester",
"click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk"
);
}
@Test
void shouldResolveAmbiguousListFallback() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.ListInjectionController")
.methodName("testList")
.build();
TriggerPoint tp1 = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.AmbiguousA")
.methodName("doAmbig")
.build();
TriggerPoint tp2 = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.AmbiguousB")
.methodName("doAmbig")
.build();
List<CallChain> chains1 = engine.findChains(List.of(ep), List.of(tp1));
List<CallChain> chains2 = engine.findChains(List.of(ep), List.of(tp2));
assertThat(chains1).isNotEmpty();
assertThat(chains1.get(0).getMethodChain()).contains(
"click.kamil.examples.statemachine.extended.web.ListInjectionController.testList",
"click.kamil.examples.statemachine.extended.service.AmbiguousA.doAmbig"
);
assertThat(chains2).isNotEmpty();
assertThat(chains2.get(0).getMethodChain()).contains(
"click.kamil.examples.statemachine.extended.web.ListInjectionController.testList",
"click.kamil.examples.statemachine.extended.service.AmbiguousB.doAmbig"
);
}
@Test
void shouldResolveConcreteReturnType() {
EntryPoint ep = EntryPoint.builder()
.className("click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController")
.methodName("testConcrete")
.build();
TriggerPoint tp = TriggerPoint.builder()
.className("click.kamil.examples.statemachine.extended.service.QualifierQuirkService")
.methodName("doQuirk")
.build();
List<CallChain> chains = engine.findChains(List.of(ep), List.of(tp));
System.out.println("CHAINS FOUND: " + chains.size()); assertThat(chains).hasSize(1);
assertThat(chains.get(0).getMethodChain()).contains(
"click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete",
"click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk"
);
}
}

View File

@@ -0,0 +1,71 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class LambdaBugTest {
@Test
void shouldResolveEventThroughLambda() throws IOException {
String source = """
package com.example;
import java.util.function.Supplier;
public class MyController {
private StateMachine machine;
public void process() {
// Supplying the event via a lambda
fireEvent(() -> TransitionEnum.STATE_L);
}
private void fireEvent(Supplier<TransitionEnum> eventSupplier) {
machine.fire(eventSupplier.get());
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum TransitionEnum { STATE_L, STATE_M }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_lambda");
Files.writeString(tempDir.resolve("Config.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.MyController")
.methodName("process")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("Poly events from lambda: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
// It SHOULD resolve to STATE_L by tracking the lambda supplier
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_L");
}
}

View File

@@ -0,0 +1,122 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class LocalVariableTest {
@Test
void shouldTraceLocalVariableInGetter(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
public OrderEvents getType() {
OrderEvents e = OrderEvents.PAY;
return e;
}
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.contains("OrderEvents.PAY")
.doesNotContain("e"); // Should not blindly add the variable name
}
@Test
void shouldTraceMultipleAssignments(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
public OrderEvents getType() {
OrderEvents e = null;
if (System.currentTimeMillis() % 2 == 0) {
e = OrderEvents.PAY;
} else {
e = OrderEvents.CANCEL;
}
return e;
}
}
enum OrderEvents { PAY, CANCEL }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCEL");
}
}

View File

@@ -0,0 +1,69 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class NewBugTest {
@Test
void shouldResolveMapLookup() throws IOException {
String source = """
package com.example;
import java.util.Map;
public class Controller {
private StateMachine machine;
private static final Map<InputEnum, TransitionEnum> MAP = Map.of(
InputEnum.IN_A, TransitionEnum.STATE_X,
InputEnum.IN_B, TransitionEnum.STATE_Y
);
public void process(InputEnum input) {
TransitionEnum trans = MAP.get(input);
machine.fire(trans);
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum InputEnum { IN_A, IN_B }
enum TransitionEnum { STATE_X, STATE_Y }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_map");
Files.writeString(tempDir.resolve("Config.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.Controller")
.methodName("process")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("Poly events: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("TransitionEnum.STATE_X", "TransitionEnum.STATE_Y");
}
}

View File

@@ -0,0 +1,86 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class OverloadedConstructorBugTest {
@Test
void shouldResolveEventThroughOverloadedConstructors() throws IOException {
String source = """
package com.example;
public class MyController {
private StateMachine machine;
public void process() {
// Creating command via 1-arg constructor
MyCommand command = new MyCommand(TransitionEnum.STATE_Z);
machine.fire(command.getEvent());
}
}
class MyCommand {
private final TransitionEnum event;
private final String metadata;
// 1-arg constructor calls 2-arg constructor using this(...)
public MyCommand(TransitionEnum event) {
this(event, "default_metadata");
}
// 2-arg constructor assigns the field
public MyCommand(TransitionEnum event, String metadata) {
this.event = event;
this.metadata = metadata;
}
public TransitionEnum getEvent() {
return event;
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum TransitionEnum { STATE_X, STATE_Y, STATE_Z }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_constructor");
Files.writeString(tempDir.resolve("Config.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.MyController")
.methodName("process")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("Poly events from overloaded constructor: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
// It SHOULD resolve to STATE_Z by following the 'this(...)' call in the constructor
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_Z");
}
}

View File

@@ -0,0 +1,66 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ReturnStatementTest {
@Test
void shouldHandleAssignmentInReturn(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class RichOrderEvent {
private OrderEvents type;
public OrderEvents getType() {
return this.type = OrderEvents.PAY;
}
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.PAY");
}
}

View File

@@ -0,0 +1,84 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class StaticFactoryBugTest {
@Test
void shouldResolveEventThroughStaticFactory() throws IOException {
String source = """
package com.example;
public class MyController {
private StateMachine machine;
public void process() {
// Creating command via static factory method
MyCommand command = MyCommand.of(TransitionEnum.STATE_W);
machine.fire(command.getEvent());
}
}
class MyCommand {
private final TransitionEnum event;
// Private constructor
private MyCommand(TransitionEnum event) {
this.event = event;
}
// Static factory method
public static MyCommand of(TransitionEnum event) {
return new MyCommand(event);
}
public TransitionEnum getEvent() {
return event;
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum TransitionEnum { STATE_W, STATE_X, STATE_Y, STATE_Z }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_static_factory");
Files.writeString(tempDir.resolve("Config.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.MyController")
.methodName("process")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("Poly events from static factory: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
// It SHOULD resolve to STATE_W by following the static factory and its internal instantiation
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_W");
}
}

View File

@@ -0,0 +1,66 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class StreamApiTests {
@TempDir
Path tempDir;
@Test
void shouldHandleStreamApiChains() throws Exception {
String source = """
package com.example;
import java.util.List;
public class StreamService {
private EventService service;
public void process(List<MyEvent> events) {
events.stream()
.filter(e -> e.isValid())
.map(e -> e.toEvent())
.forEach(t -> service.trigger(t.getEvent()));
}
}
class EventService {
public void trigger(String ev) {}
}
class MyEvent {
public boolean isValid() { return true; }
public OrderEvent toEvent() { return new OrderEvent(); }
}
class OrderEvent {
public String getEvent() { return "STREAM_EVENT"; }
}
""";
Files.writeString(tempDir.resolve("StreamService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("StreamService.java")
.sourceModule("com.example")
.event("t.getEvent()")
.lineNumber(11)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.StreamService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
System.out.println("TEST RESULT: " + result);
System.out.println("POLY EVENTS: " + result.getPolymorphicEvents());
assertThat(result.getPolymorphicEvents()).isNotNull().containsExactlyInAnyOrder("STREAM_EVENT");
}
}

View File

@@ -0,0 +1,75 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class SuperMethodBugTest {
@Test
void shouldResolveSuperMethodCorrectly() throws IOException {
String source = """
package com.example;
public class ChildController extends ParentController {
private StateMachine machine;
@Override
public TransitionEnum getEvent() {
return TransitionEnum.STATE_X; // Child returns X
}
public void process() {
// Calls super.getEvent() which returns Y, NOT X!
machine.fire(super.getEvent());
}
}
class ParentController {
public TransitionEnum getEvent() {
return TransitionEnum.STATE_Y; // Parent returns Y
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum TransitionEnum { STATE_X, STATE_Y }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_super");
Files.writeString(tempDir.resolve("Config.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.ChildController")
.methodName("process")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("Poly events from super: " + chains.get(0).getTriggerPoint().getPolymorphicEvents());
// It SHOULD resolve to STATE_Y because we called super.getEvent()
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_Y");
}
}

View File

@@ -0,0 +1,71 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SuperMethodInvocationTest {
@Test
void shouldHandleSuperMethodInvocation(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(DerivedEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
class BaseEvent {
public OrderEvents getType() {
return OrderEvents.PAY;
}
}
class DerivedEvent extends BaseEvent {
public OrderEvents getType() {
return super.getType();
}
}
enum OrderEvents { PAY }
""";
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.contains("OrderEvents.PAY");
}
}

View File

@@ -0,0 +1,89 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class SpringContextScannerIntegrationTest {
private CodebaseContext context;
private SpringBeanRegistry registry;
@BeforeEach
void setUp() throws IOException {
Path projectRoot = Path.of("../state_machines/extended_analysis_sample").toAbsolutePath().normalize();
context = new CodebaseContext();
context.setProjectRoot(projectRoot);
context.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> cp = resolver.resolveClasspath(projectRoot);
if (!cp.isEmpty()) {
context.setClasspath(cp);
}
context.setResolveBindings(true);
context.scan(Set.of(projectRoot), Collections.emptySet());
registry = new SpringBeanRegistry();
SpringContextScanner scanner = new SpringContextScanner(registry);
for (var cu : context.getCompilationUnits()) {
cu.accept(scanner);
}
}
@Test
void shouldFindStereotypeBeans() {
List<SpringBean> beans = registry.getBeans();
// PrimaryQuirkService
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("click.kamil.examples.statemachine.extended.service.PrimaryQuirkService");
assertThat(bean.isPrimary()).isTrue();
assertThat(bean.getAssignableTypes()).contains("click.kamil.examples.statemachine.extended.service.QuirkService");
assertThat(bean.getBeanNames()).contains("primaryQuirkService");
});
// QualifierQuirkService
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("click.kamil.examples.statemachine.extended.service.QualifierQuirkService");
assertThat(bean.isPrimary()).isFalse();
assertThat(bean.getBeanNames()).contains("qualifierQuirkService");
});
// NamedQuirkService (should have explicit name via @Service("customName"))
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("click.kamil.examples.statemachine.extended.service.NamedQuirkService");
assertThat(bean.getBeanNames()).contains("customName");
});
// PaymentController (should be detected via @RestController)
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("click.kamil.examples.statemachine.extended.web.PaymentController");
});
}
@Test
void shouldFindMethodBeans() {
List<SpringBean> beans = registry.getBeans();
// Beans created via @Bean
assertThat(beans).anySatisfy(bean -> {
assertThat(bean.getTypeFqn()).isEqualTo("java.lang.String");
assertThat(bean.getFactoryMethodName()).isEqualTo("myStringBean");
assertThat(bean.getBeanNames()).contains("customMockBean");
assertThat(bean.getDeclaringClassFqn()).isEqualTo("click.kamil.examples.statemachine.extended.config.MockBeanConfig");
});
}
}

View File

@@ -0,0 +1,220 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class SpringDependencyResolverTest {
private SpringBeanRegistry registry;
private SpringDependencyResolver resolver;
@BeforeEach
void setUp() {
registry = new SpringBeanRegistry();
resolver = new SpringDependencyResolver(registry);
}
@Test
void shouldResolveSingleMatchByType() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.PaymentServiceImpl")
.assignableTypes(Set.of("com.example.PaymentService"))
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "paymentService");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.PaymentServiceImpl");
}
@Test
void shouldResolveByPrimaryWhenMultipleImplementationsExist() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.NormalService")
.assignableTypes(Set.of("com.example.PaymentService"))
.isPrimary(false)
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.PrimaryService")
.assignableTypes(Set.of("com.example.PaymentService"))
.isPrimary(true)
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "paymentService");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.PrimaryService");
}
@Test
void shouldResolveByQualifierWhenMultipleImplementationsExist() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceA")
.assignableTypes(Set.of("com.example.PaymentService"))
.qualifiers(Set.of("serviceA"))
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceB")
.assignableTypes(Set.of("com.example.PaymentService"))
.qualifiers(Set.of("serviceB"))
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", "serviceB", "paymentService");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.ServiceB");
}
@Test
void shouldResolveByInjectionNameFallback() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceA")
.assignableTypes(Set.of("com.example.PaymentService"))
.beanNames(Set.of("serviceA"))
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceB")
.assignableTypes(Set.of("com.example.PaymentService"))
.beanNames(Set.of("specialService"))
.build());
// Qualifier is null, Primary is false. Should match by variable name "specialService"
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "specialService");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.ServiceB");
}
@Test
void shouldResolveByOrderWhenOtherFiltersTie() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.LowestOrderService")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(Integer.MAX_VALUE) // Lowest precedence
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.HighestOrderService")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(Integer.MIN_VALUE) // Highest precedence
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.MiddleOrderService")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(0)
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "someUnknownName");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.HighestOrderService");
}
@Test
void shouldResolveByOrderWithCustomValues() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceA")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(400)
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceB")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(100)
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceC")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(2147483647 + 400) // This wraps around in Java due to overflow, but let's test a very large valid value
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "someUnknownName");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.ServiceC");
}
@Test
void primaryShouldOverrideOrder() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.PrimaryButLowestOrder")
.assignableTypes(Set.of("com.example.PaymentService"))
.isPrimary(true)
.order(Integer.MAX_VALUE)
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.NotPrimaryButHighestOrder")
.assignableTypes(Set.of("com.example.PaymentService"))
.isPrimary(false)
.order(Integer.MIN_VALUE)
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "someUnknownName");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.PrimaryButLowestOrder");
}
@Test
void qualifierShouldOverridePrimaryAndOrder() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.PrimaryService")
.assignableTypes(Set.of("com.example.PaymentService"))
.isPrimary(true)
.order(Integer.MIN_VALUE)
.qualifiers(Set.of("primary"))
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.QualifiedService")
.assignableTypes(Set.of("com.example.PaymentService"))
.isPrimary(false)
.order(Integer.MAX_VALUE)
.qualifiers(Set.of("targetQualifier"))
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", "targetQualifier", "someUnknownName");
assertThat(resolved).hasSize(1);
assertThat(resolved.get(0).getTypeFqn()).isEqualTo("com.example.QualifiedService");
}
@Test
void shouldReturnMultipleWhenOrderIsTied() {
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceA")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(10)
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceB")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(10)
.build());
registry.addBean(SpringBean.builder()
.typeFqn("com.example.ServiceC")
.assignableTypes(Set.of("com.example.PaymentService"))
.order(20)
.build());
List<SpringBean> resolved = resolver.resolve("com.example.PaymentService", null, "someUnknownName");
assertThat(resolved).hasSize(2);
assertThat(resolved.stream().map(SpringBean::getTypeFqn))
.containsExactlyInAnyOrder("com.example.ServiceA", "com.example.ServiceB");
}
}

View File

@@ -261,6 +261,56 @@ class AstTransitionParserTest {
.contains("System.out.println(\"Hello\")");
}
@Test
void shouldExtractInternalLogicForSameFileMethodReturningLambda() {
String source = """
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.guard(guardVarEquals("test"));
}
protected Guard<String, String> guardVarEquals(String expected) {
return context -> {
return expected.equals(context.getMessage());
};
}
}
""";
MethodDeclaration method = createMethodDeclarationWithBindings(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
assertThat(transitions.getFirst().getGuard().internalLogic())
.contains("return expected.equals(context.getMessage());")
.doesNotContain("protected Guard<String, String> guardVarEquals");
}
private MethodDeclaration createMethodDeclarationWithBindings(String source) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(true);
parser.setBindingsRecovery(true);
parser.setEnvironment(new String[0], new String[0], null, true);
parser.setUnitName("TestClass.java");
parser.setSource(source.toCharArray());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().getFirst();
MethodDeclaration[] methods = typeDecl.getMethods();
if (methods.length > 0) {
return methods[0];
}
return null;
}
private MethodDeclaration createMethodDeclaration(String source) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setKind(ASTParser.K_COMPILATION_UNIT);

View File

@@ -0,0 +1,92 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class BindingResolverTest {
@Test
void testBindingResolverMethods(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Base.java"),
"package com.foo;\n" +
"public interface Base {\n" +
" void run();\n" +
"}"
);
Files.writeString(comFoo.resolve("Child.java"),
"package com.foo;\n" +
"public class Child implements Base {\n" +
" public void run() {}\n" +
" public void execute() {\n" +
" Child c = new Child();\n" +
" c.run();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
assertThat(childTd).isNotNull();
ITypeBinding binding = BindingResolver.resolveType(childTd);
assertThat(binding).isNotNull();
assertThat(BindingResolver.getFullyQualifiedName(binding)).isEqualTo("com.foo.Child");
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.Base")).isTrue();
assertThat(BindingResolver.isSubtypeOf(binding, "java.lang.Object")).isTrue();
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.NonExistent")).isFalse();
// Find MethodDeclaration execute
MethodDeclaration executeMethod = null;
for (MethodDeclaration md : childTd.getMethods()) {
if (md.getName().getIdentifier().equals("execute")) {
executeMethod = md;
}
}
assertThat(executeMethod).isNotNull();
// Traverse the body to find ClassInstanceCreation and MethodInvocation
class Visitor extends ASTVisitor {
ClassInstanceCreation cic = null;
MethodInvocation mi = null;
@Override
public boolean visit(ClassInstanceCreation node) {
cic = node;
return super.visit(node);
}
@Override
public boolean visit(MethodInvocation node) {
mi = node;
return super.visit(node);
}
}
Visitor visitor = new Visitor();
executeMethod.accept(visitor);
assertThat(visitor.cic).isNotNull();
IMethodBinding constrBinding = BindingResolver.resolveConstructor(visitor.cic);
assertThat(constrBinding).isNotNull();
assertThat(constrBinding.isConstructor()).isTrue();
assertThat(visitor.mi).isNotNull();
IMethodBinding methodBinding = BindingResolver.resolveMethod(visitor.mi);
assertThat(methodBinding).isNotNull();
assertThat(methodBinding.getName()).isEqualTo("run");
}
}

View File

@@ -0,0 +1,69 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ControlFlowGraphTest {
@Test
void testSequentialAndBranchingCfg(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run(boolean flag) {\n" +
" int a = 1;\n" +
" if (flag) {\n" +
" a = 2;\n" +
" } else {\n" +
" a = 3;\n" +
" }\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
assertThat(cfg).isNotNull();
assertThat(cfg.getEntryNode().getSuccessors()).hasSize(1);
// Entry goes to first statement: 'int a = 1;'
ControlFlowGraph.CfgNode firstStmtNode = cfg.getEntryNode().getSuccessors().iterator().next();
assertThat(firstStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
// First stmt goes to condition of 'if (flag)'
assertThat(firstStmtNode.getSuccessors()).hasSize(1);
ControlFlowGraph.CfgNode condNode = firstStmtNode.getSuccessors().iterator().next();
assertThat(condNode.getAstNode()).isInstanceOf(SimpleName.class); // flag
// Cond node goes to two branches: 'a = 2;' and 'a = 3;'
assertThat(condNode.getSuccessors()).hasSize(2);
for (ControlFlowGraph.CfgNode branchSuccessor : condNode.getSuccessors()) {
assertThat(branchSuccessor.getAstNode()).isInstanceOf(ExpressionStatement.class); // assignment expression statement
// Each branch successor should flow into the next sequential statement: 'int b = a;'
assertThat(branchSuccessor.getSuccessors()).hasSize(1);
ControlFlowGraph.CfgNode nextStmtNode = branchSuccessor.getSuccessors().iterator().next();
assertThat(nextStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
assertThat(nextStmtNode.getSuccessors()).contains(cfg.getExitNode());
}
}
}

View File

@@ -0,0 +1,677 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class JdtDataFlowModelTest {
@Test
void testRecursiveJdtDataFlowResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run() {\n" +
" String a = \"VAL1\";\n" +
" String b = a;\n" +
" String c = b;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement cStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment cFrag = (VariableDeclarationFragment) cStmt.fragments().get(0);
Expression initializer = cFrag.getInitializer(); // b
DataFlowModel model = new JdtDataFlowModel(context);
List<Expression> defs = model.getReachingDefinitions(initializer);
assertThat(defs).isNotEmpty();
Expression singleDef = defs.get(defs.size() - 1);
assertThat(singleDef).isInstanceOf(StringLiteral.class);
assertThat(((StringLiteral) singleDef).getLiteralValue()).isEqualTo("VAL1");
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("VAL1");
}
@Test
void testBranchDivergenceJdtDataFlowResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run(boolean flag) {\n" +
" String a = \"SAME\";\n" +
" if (flag) {\n" +
" a = \"SAME\";\n" +
" } else {\n" +
" a = \"SAME\";\n" +
" }\n" +
" String b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer(); // a
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
// Both branches assign "SAME", so it converges to "SAME"
assertThat(val).isEqualTo("SAME");
}
@Test
void testFieldConstantPropagation(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" private static final String MY_CONST = \"CONST_VAL\";\n" +
" public void run() {\n" +
" String a = MY_CONST;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement aStmt = (VariableDeclarationStatement) md.getBody().statements().get(0);
VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0);
Expression initializer = aFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("CONST_VAL");
}
@Test
void testTernaryExpressionResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run(boolean flag) {\n" +
" String a = flag ? \"TRUE_VAL\" : \"FALSE_VAL\";\n" +
" String b = true ? \"TRUE_VAL\" : \"FALSE_VAL\";\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
// 1. Unresolvable condition (flag)
VariableDeclarationStatement aStmt = (VariableDeclarationStatement) md.getBody().statements().get(0);
VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0);
Expression initA = aFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
List<Expression> defsA = model.getReachingDefinitions(initA);
assertThat(defsA).hasSize(1);
assertThat(defsA.get(0)).isInstanceOf(ConditionalExpression.class);
// 2. Statically resolvable condition (true)
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initB = bFrag.getInitializer();
List<Expression> defsB = model.getReachingDefinitions(initB);
assertThat(defsB).hasSize(1);
assertThat(defsB.get(0)).isInstanceOf(StringLiteral.class);
assertThat(((StringLiteral) defsB.get(0)).getLiteralValue()).isEqualTo("TRUE_VAL");
}
@Test
void testStaticFactoryMethodTracking(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public static String create(String value) {\n" +
" return value;\n" +
" }\n" +
" public void run() {\n" +
" String a = create(\"FACTORY_VAL\");\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration runMd = null;
for (MethodDeclaration md : td.getMethods()) {
if (md.getName().getIdentifier().equals("run")) {
runMd = md;
}
}
assertThat(runMd).isNotNull();
VariableDeclarationStatement aStmt = (VariableDeclarationStatement) runMd.getBody().statements().get(0);
VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0);
Expression initializer = aFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("FACTORY_VAL");
}
@Test
void testDoWhileLoopFlow(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run(boolean flag) {\n" +
" int a = 1;\n" +
" do {\n" +
" a = 2;\n" +
" } while (flag);\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
// Find the condition node (flag)
ControlFlowGraph.CfgNode condNode = null;
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
if (node.getAstNode() instanceof SimpleName sn && sn.getIdentifier().equals("flag")) {
condNode = node;
break;
}
}
assertThat(condNode).isNotNull();
// The condition node should loop back to the statement inside the body (a = 2;)
boolean hasLoopBackToBody = false;
for (ControlFlowGraph.CfgNode succ : condNode.getSuccessors()) {
if (succ.getAstNode() instanceof ExpressionStatement es) {
if (es.getExpression() instanceof Assignment assign) {
if (assign.getLeftHandSide() instanceof SimpleName sn && sn.getIdentifier().equals("a")) {
hasLoopBackToBody = true;
}
}
}
}
assertThat(hasLoopBackToBody).isTrue();
}
@Test
void testConstructorAssignmentPropagation(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" private final String myVar;\n" +
" public Test() {\n" +
" this.myVar = \"CONSTRUCTOR_VAL\";\n" +
" }\n" +
" public void run() {\n" +
" String a = myVar;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement aStmt = (VariableDeclarationStatement) md.getBody().statements().get(0);
VariableDeclarationFragment aFrag = (VariableDeclarationFragment) aStmt.fragments().get(0);
Expression initializer = aFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("CONSTRUCTOR_VAL");
}
@Test
void testObjectSensitiveInstanceFieldTracing(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public static class Builder {\n" +
" private final String val;\n" +
" public Builder(String val) {\n" +
" this.val = val;\n" +
" }\n" +
" public String getVal() {\n" +
" return this.val;\n" +
" }\n" +
" }\n" +
" public void run() {\n" +
" Builder b = new Builder(\"OBJECT_SENSITIVE\");\n" +
" String res = b.getVal();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement resStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment resFrag = (VariableDeclarationFragment) resStmt.fragments().get(0);
Expression initializer = resFrag.getInitializer(); // b.getVal()
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("OBJECT_SENSITIVE");
}
@Test
void testConstructorChaining(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public static class Command {\n" +
" private final String event;\n" +
" private final String info;\n" +
" public Command(String event, String info) {\n" +
" this.event = event;\n" +
" this.info = info;\n" +
" }\n" +
" public Command(String event) {\n" +
" this(event, \"DEFAULT_INFO\");\n" +
" }\n" +
" public Command() {\n" +
" this(\"DEFAULT_EVENT\");\n" +
" }\n" +
" public String getEvent() { return this.event; }\n" +
" public String getInfo() { return this.info; }\n" +
" }\n" +
" public void run() {\n" +
" Command cmd = new Command();\n" +
" String ev = cmd.getEvent();\n" +
" String inf = cmd.getInfo();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement evStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment evFrag = (VariableDeclarationFragment) evStmt.fragments().get(0);
Expression initEv = evFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String valEv = model.resolveValue(initEv, context);
assertThat(valEv).isEqualTo("DEFAULT_EVENT");
VariableDeclarationStatement infStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment infFrag = (VariableDeclarationFragment) infStmt.fragments().get(0);
Expression initInf = infFrag.getInitializer();
String valInf = model.resolveValue(initInf, context);
assertThat(valInf).isEqualTo("DEFAULT_INFO");
}
@Test
void testExplicitSuperCalls(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public static class Base {\n" +
" public String getValue() {\n" +
" return \"BASE_VAL\";\n" +
" }\n" +
" }\n" +
" public static class Sub extends Base {\n" +
" @Override\n" +
" public String getValue() {\n" +
" return super.getValue();\n" +
" }\n" +
" }\n" +
" public void run() {\n" +
" Sub sub = new Sub();\n" +
" String val = sub.getValue();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement valStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment valFrag = (VariableDeclarationFragment) valStmt.fragments().get(0);
Expression initVal = valFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initVal, context);
assertThat(val).isEqualTo("BASE_VAL");
}
@Test
void testDynamicDispatchNarrowing(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public interface Service {\n" +
" String getEvent();\n" +
" }\n" +
" public static class PayService implements Service {\n" +
" public String getEvent() { return \"PAY\"; }\n" +
" }\n" +
" public static class CancelService implements Service {\n" +
" public String getEvent() { return \"CANCEL\"; }\n" +
" }\n" +
" public void run() {\n" +
" Service s = new PayService();\n" +
" String ev = s.getEvent();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement evStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment evFrag = (VariableDeclarationFragment) evStmt.fragments().get(0);
Expression initEv = evFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
List<Expression> defs = model.getReachingDefinitions(initEv);
assertThat(defs).isNotEmpty();
String val = model.resolveValue(initEv, context);
assertThat(val).isEqualTo("PAY");
}
@Test
void testFlowSensitiveLocalFieldMutations(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public static class Command {\n" +
" private String event = \"DEFAULT\";\n" +
" public void setEvent(String event) {\n" +
" this.event = event;\n" +
" }\n" +
" public String getEvent() {\n" +
" return this.event;\n" +
" }\n" +
" }\n" +
" public void run() {\n" +
" Command cmd = new Command();\n" +
" cmd.setEvent(\"PAY\");\n" +
" String res = cmd.getEvent();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement resStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment resFrag = (VariableDeclarationFragment) resStmt.fragments().get(0);
Expression initializer = resFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("PAY");
}
@Test
void testFluentBuilderPatternPropagation(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public static class EventWrapper {\n" +
" private final String event;\n" +
" private EventWrapper(String event) {\n" +
" this.event = event;\n" +
" }\n" +
" public String getEvent() { return this.event; }\n" +
" public static Builder builder() {\n" +
" return new Builder();\n" +
" }\n" +
" public static class Builder {\n" +
" private String event;\n" +
" public Builder event(String event) {\n" +
" this.event = event;\n" +
" return this;\n" +
" }\n" +
" public EventWrapper build() {\n" +
" return new EventWrapper(this.event);\n" +
" }\n" +
" }\n" +
" }\n" +
" public void run() {\n" +
" EventWrapper w = EventWrapper.builder().event(\"PAY_SUCCESS\").build();\n" +
" String res = w.getEvent();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement resStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment resFrag = (VariableDeclarationFragment) resStmt.fragments().get(0);
Expression initializer = resFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("PAY_SUCCESS");
}
@Test
void testPolymorphicUnionFallback(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public interface Service {\n" +
" String getEvent();\n" +
" }\n" +
" public static class PayService implements Service {\n" +
" public String getEvent() { return \"PAY\"; }\n" +
" }\n" +
" public static class CancelService implements Service {\n" +
" public String getEvent() { return \"CANCEL\"; }\n" +
" }\n" +
" public void run(Service s) {\n" +
" String ev = s.getEvent();\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = null;
for (MethodDeclaration m : td.getMethods()) {
if (m.getName().getIdentifier().equals("run")) {
md = m;
}
}
assertThat(md).isNotNull();
VariableDeclarationStatement evStmt = (VariableDeclarationStatement) md.getBody().statements().get(0);
VariableDeclarationFragment evFrag = (VariableDeclarationFragment) evStmt.fragments().get(0);
Expression initEv = evFrag.getInitializer();
DataFlowModel model = new JdtDataFlowModel(context);
List<Expression> defs = model.getReachingDefinitions(initEv);
assertThat(defs).hasSize(2);
List<String> values = defs.stream()
.filter(e -> e instanceof StringLiteral)
.map(e -> ((StringLiteral) e).getLiteralValue())
.toList();
assertThat(values).containsExactlyInAnyOrder("PAY", "CANCEL");
}
}

View File

@@ -0,0 +1,56 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class LegacyDataFlowModelTest {
@Test
void testLegacyDataFlowResolution(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run() {\n" +
" String a = \"VAL1\";\n" +
" String b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
VariableDeclarationFragment frag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = frag.getInitializer(); // 'a' reference
VariableTracer tracer = new VariableTracer(context, new ConstantResolver());
DataFlowModel model = new LegacyDataFlowModel(tracer);
List<Expression> defs = model.getReachingDefinitions(initializer);
assertThat(defs).isNotEmpty();
Expression resolvedExpr = defs.get(defs.size() - 1);
assertThat(resolvedExpr).isInstanceOf(StringLiteral.class);
assertThat(((StringLiteral) resolvedExpr).getLiteralValue()).isEqualTo("VAL1");
String val = model.resolveValue(initializer, context);
assertThat(val).isEqualTo("VAL1");
}
}

View File

@@ -0,0 +1,105 @@
package click.kamil.springstatemachineexporter.ast.common;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
class ReachingDefinitionsTest {
@Test
void testReachingDefinitionsBranchMerge(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run(boolean flag) {\n" +
" int a = 1;\n" +
" if (flag) {\n" +
" a = 2;\n" +
" } else {\n" +
" a = 3;\n" +
" }\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
ReachingDefinitions rd = new ReachingDefinitions(cfg);
// Find usage 'a' in 'int b = a;'
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer(); // simpleName 'a'
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
assertThat(defs).hasSize(2);
Set<String> values = defs.stream()
.map(ReachingDefinitions::getDefinitionExpression)
.map(expr -> {
if (expr instanceof NumberLiteral nl) {
return nl.getToken();
}
return expr.toString();
})
.collect(Collectors.toSet());
assertThat(values).containsExactlyInAnyOrder("2", "3");
}
@Test
void testReachingDefinitionsKilled(@TempDir Path tempDir) throws IOException {
Path comFoo = tempDir.resolve("com/foo");
Files.createDirectories(comFoo);
Files.writeString(comFoo.resolve("Test.java"),
"package com.foo;\n" +
"public class Test {\n" +
" public void run() {\n" +
" int a = 1;\n" +
" a = 2;\n" +
" int b = a;\n" +
" }\n" +
"}"
);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
assertThat(td).isNotNull();
MethodDeclaration md = td.getMethods()[0];
ControlFlowGraph cfg = CfgBuilder.build(md);
ReachingDefinitions rd = new ReachingDefinitions(cfg);
// Find usage 'a' in 'int b = a;'
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
Expression initializer = bFrag.getInitializer();
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
assertThat(defs).hasSize(1);
ASTNode singleDef = defs.iterator().next();
Expression defExpr = ReachingDefinitions.getDefinitionExpression(singleDef);
assertThat(defExpr).isInstanceOf(NumberLiteral.class);
assertThat(((NumberLiteral) defExpr).getToken()).isEqualTo("2");
}
}

View File

@@ -192,7 +192,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 16,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PLACE_ORDER" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -227,7 +227,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21,
"polymorphicEvents" : null
"polymorphicEvents" : [ "CANCEL_ORDER" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -288,7 +288,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : [ "PAY_ORDER" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -21,10 +21,30 @@
"lineNumber" : 16,
"polymorphicEvents" : null
}, {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"event" : "FALLBACK_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "PROFILED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
}, {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
@@ -60,6 +80,56 @@
"sourceState" : null,
"lineNumber" : 29,
"polymorphicEvents" : null
}, {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 22,
"polymorphicEvents" : null
}, {
"event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 35,
"polymorphicEvents" : null
}, {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : null,
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 28,
"polymorphicEvents" : null
}, {
"event" : "REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
@@ -144,39 +214,137 @@
"interceptorType" : "Spring MVC Interceptor"
},
"parameters" : [ ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/submit",
"className" : "click.kamil.examples.statemachine.extended.web.OrderController",
"methodName" : "submitOrder",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/submit",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-setter",
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
"methodName" : "testSetter",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
"metadata" : {
"path" : "/test-setter",
"verb" : "GET"
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
"triggerPoint" : {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/primary",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testPrimary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/primary",
"verb" : "POST"
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "START",
"targetState" : "PROCESSING",
"event" : "EXTERNAL_TRIGGER"
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/named",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testNamed",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/named",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/qualifier",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testQualifier",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/qualifier",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/fallback",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testFallback",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/fallback",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-order",
"className" : "click.kamil.examples.statemachine.extended.web.OrderedTestController",
"methodName" : "testOrder",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderedTestController.java",
"metadata" : {
"path" : "/test-order",
"verb" : "GET"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-concrete",
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
"methodName" : "testConcrete",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
"metadata" : {
"path" : "/test-concrete",
"verb" : "GET"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-field",
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
"methodName" : "testField",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
"metadata" : {
"path" : "/test-field",
"verb" : "GET"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-list",
"className" : "click.kamil.examples.statemachine.extended.web.ListInjectionController",
"methodName" : "testList",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ListInjectionController.java",
"metadata" : {
"path" : "/test-list",
"verb" : "GET"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /api/base/{id}",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "processBaseEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/base/{id}",
"verb" : "GET"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/submit",
@@ -199,7 +367,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20,
"polymorphicEvents" : null
"polymorphicEvents" : [ "SUBMIT_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -230,7 +398,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : [ "CANCEL_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -327,7 +495,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : [ "REACTIVE_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -365,6 +533,288 @@
"targetState" : "START",
"event" : "AUDIT_EVENT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /test-setter",
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
"methodName" : "testSetter",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
"metadata" : {
"path" : "/test-setter",
"verb" : "GET"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/primary",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testPrimary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/primary",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/named",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testNamed",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/named",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/qualifier",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testQualifier",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/qualifier",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/fallback",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testFallback",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/fallback",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /test-concrete",
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
"methodName" : "testConcrete",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
"metadata" : {
"path" : "/test-concrete",
"verb" : "GET"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /test-field",
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
"methodName" : "testField",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
"metadata" : {
"path" : "/test-field",
"verb" : "GET"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /api/base/{id}",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "processBaseEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/base/{id}",
"verb" : "GET"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.processBaseEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.processPayment" ],
"triggerPoint" : {
"event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 22,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 35,
"polymorphicEvents" : null
},
"contextMachineId" : "paymentId",
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : null,
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 28,
"polymorphicEvents" : null
},
"contextMachineId" : "paymentId",
"matchedTransitions" : null
} ],
"properties" : {
"default" : {

View File

@@ -21,10 +21,30 @@
"lineNumber" : 16,
"polymorphicEvents" : null
}, {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"event" : "FALLBACK_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.FallbackQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/FallbackQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "PROFILED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ProfiledQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ProfiledQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
}, {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
@@ -60,6 +80,56 @@
"sourceState" : null,
"lineNumber" : 29,
"polymorphicEvents" : null
}, {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 22,
"polymorphicEvents" : null
}, {
"event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 35,
"polymorphicEvents" : null
}, {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : null,
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 28,
"polymorphicEvents" : null
}, {
"event" : "REACTIVE_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
@@ -144,39 +214,137 @@
"interceptorType" : "Spring MVC Interceptor"
},
"parameters" : [ ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/submit",
"className" : "click.kamil.examples.statemachine.extended.web.OrderController",
"methodName" : "submitOrder",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderController.java",
"metadata" : {
"path" : "/api/orders/submit",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-setter",
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
"methodName" : "testSetter",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
"metadata" : {
"path" : "/test-setter",
"verb" : "GET"
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.submitOrder", "click.kamil.examples.statemachine.extended.service.OrderService.processSubmit" ],
"triggerPoint" : {
"event" : "EXTERNAL_TRIGGER",
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
"methodName" : "processSubmit",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : null
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/primary",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testPrimary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/primary",
"verb" : "POST"
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "START",
"targetState" : "PROCESSING",
"event" : "EXTERNAL_TRIGGER"
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/named",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testNamed",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/named",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/qualifier",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testQualifier",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/qualifier",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /api/quirk/fallback",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testFallback",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/fallback",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-order",
"className" : "click.kamil.examples.statemachine.extended.web.OrderedTestController",
"methodName" : "testOrder",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderedTestController.java",
"metadata" : {
"path" : "/test-order",
"verb" : "GET"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-concrete",
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
"methodName" : "testConcrete",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
"metadata" : {
"path" : "/test-concrete",
"verb" : "GET"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-field",
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
"methodName" : "testField",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
"metadata" : {
"path" : "/test-field",
"verb" : "GET"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /test-list",
"className" : "click.kamil.examples.statemachine.extended.web.ListInjectionController",
"methodName" : "testList",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ListInjectionController.java",
"metadata" : {
"path" : "/test-list",
"verb" : "GET"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "GET /api/base/{id}",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "processBaseEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/base/{id}",
"verb" : "GET"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
}, {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/orders/submit",
@@ -199,7 +367,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 20,
"polymorphicEvents" : null
"polymorphicEvents" : [ "SUBMIT_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -230,7 +398,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : null
"polymorphicEvents" : [ "CANCEL_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -327,7 +495,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 18,
"polymorphicEvents" : null
"polymorphicEvents" : [ "REACTIVE_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -365,6 +533,288 @@
"targetState" : "START",
"event" : "AUDIT_EVENT"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /test-setter",
"className" : "click.kamil.examples.statemachine.extended.web.SetterInjectionController",
"methodName" : "testSetter",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/SetterInjectionController.java",
"metadata" : {
"path" : "/test-setter",
"verb" : "GET"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/primary",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testPrimary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/primary",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/named",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testNamed",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/named",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/qualifier",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testQualifier",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/qualifier",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/quirk/fallback",
"className" : "click.kamil.examples.statemachine.extended.web.QuirkController",
"methodName" : "testFallback",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/QuirkController.java",
"metadata" : {
"path" : "/api/quirk/fallback",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "PRIMARY_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 19,
"polymorphicEvents" : [ "PRIMARY_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /test-concrete",
"className" : "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController",
"methodName" : "testConcrete",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/ConcreteReturnTypeController.java",
"metadata" : {
"path" : "/test-concrete",
"verb" : "GET"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "QUALIFIER_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "QUALIFIER_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /test-field",
"className" : "click.kamil.examples.statemachine.extended.web.FieldInjectionController",
"methodName" : "testField",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/FieldInjectionController.java",
"metadata" : {
"path" : "/test-field",
"verb" : "GET"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
"triggerPoint" : {
"event" : "NAMED_EVENT",
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
"methodName" : "doQuirk",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "NAMED_EVENT" ]
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "GET /api/base/{id}",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "processBaseEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/base/{id}",
"verb" : "GET"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.processBaseEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.processPayment" ],
"triggerPoint" : {
"event" : "AUTHORIZE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "processPayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 22,
"polymorphicEvents" : null
},
"contextMachineId" : null,
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "CAPTURE",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 35,
"polymorphicEvents" : null
},
"contextMachineId" : "paymentId",
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /api/payment/{id}/capture",
"className" : "click.kamil.examples.statemachine.extended.web.PaymentController",
"methodName" : "capturePaymentEndpoint",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/PaymentController.java",
"metadata" : {
"path" : "/api/payment/{id}/capture",
"verb" : "POST"
},
"parameters" : [ {
"name" : "id",
"type" : "String",
"annotations" : [ "PathVariable" ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
"triggerPoint" : {
"event" : "[LIFECYCLE:RESTORE]",
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
"methodName" : "capturePayment",
"sourceFile" : null,
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 28,
"polymorphicEvents" : null
},
"contextMachineId" : "paymentId",
"matchedTransitions" : null
} ],
"properties" : {
"default" : {

View File

@@ -57,7 +57,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 15,
"polymorphicEvents" : null
"polymorphicEvents" : [ "INHERITED_SUBMIT" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -0,0 +1,15 @@
digraph statemachine {
rankdir=LR;
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
edge [fontname="Arial", fontsize=10];
_start [shape=circle, label="", fillcolor=black, width=0.1];
_start -> SUBMITTED;
CANCELED [fillcolor=lightgray];
FULFILLED [fillcolor=lightgray];
SUBMITTED -> PAID [label="OrderEvents.PAY", style="solid", color="black"];
PAID -> FULFILLED [label="OrderEvents.FULFILL", style="solid", color="black"];
SUBMITTED -> CANCELED [label="OrderEvents.CANCEL", style="solid", color="black"];
PAID -> CANCELED [label="OrderEvents.ABCD", style="solid", color="black"];
}

View File

@@ -0,0 +1,635 @@
{
"metadata" : {
"triggers" : [ {
"event" : "event",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : null
}, {
"event" : "payload",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processPayloadEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : null
}, {
"event" : "event",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processCustomEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21,
"polymorphicEvents" : null
} ],
"entryPoints" : [ {
"type" : "REST",
"name" : "POST /pay",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "pay",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /fulfill",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "fulfill",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/fulfill",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /cancel",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "cancel",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/cancel",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /payload-pay",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payloadPay",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/payload-pay",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /pay-variable",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payVariable",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-variable",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /pay-cast",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payCast",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-cast",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /pay-ternary",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payTernary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-ternary",
"verb" : "POST"
},
"parameters" : [ {
"name" : "isPay",
"type" : "boolean",
"annotations" : [ ]
} ]
}, {
"type" : "REST",
"name" : "POST /pay-list",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payList",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-list",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /pay-builder-static",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payBuilderStatic",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-builder-static",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /pay-builder-instance",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payBuilderInstance",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-builder-instance",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /abcd",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "abcd",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/abcd",
"verb" : "POST"
},
"parameters" : [ ]
}, {
"type" : "REST",
"name" : "POST /mystery",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "mystery",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/mystery",
"verb" : "POST"
},
"parameters" : [ ]
} ],
"callChains" : [ {
"entryPoint" : {
"type" : "REST",
"name" : "POST /pay",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "pay",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.pay", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "new PayEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.PAY" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /fulfill",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "fulfill",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/fulfill",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.fulfill", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "new FulfillEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.FULFILL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.FULFILLED",
"event" : "OrderEvents.FULFILL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /cancel",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "cancel",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/cancel",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.cancel", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "new CancelEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.CANCEL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /payload-pay",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payloadPay",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/payload-pay",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payloadPay", "click.kamil.examples.statemachine.polymorphic.OrderService.processPayloadEvent" ],
"triggerPoint" : {
"event" : "new PayEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processPayloadEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 17,
"polymorphicEvents" : [ "OrderEvents.PAY" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /pay-variable",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payVariable",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-variable",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payVariable", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "new PayEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.PAY" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /pay-cast",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payCast",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-cast",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payCast", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "new PayEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.PAY" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /pay-ternary",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payTernary",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-ternary",
"verb" : "POST"
},
"parameters" : [ {
"name" : "isPay",
"type" : "boolean",
"annotations" : [ ]
} ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payTernary", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "isPay ? new PayEvent() : new CancelEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.PAY", "OrderEvents.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
}, {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.CANCEL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /pay-list",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payList",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-list",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "new PayEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.PAY" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /pay-builder-static",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payBuilderStatic",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-builder-static",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderStatic", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "new FulfillEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.FULFILL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.FULFILLED",
"event" : "OrderEvents.FULFILL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /pay-builder-instance",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "payBuilderInstance",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/pay-builder-instance",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payBuilderInstance", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ],
"triggerPoint" : {
"event" : "new CancelEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 13,
"polymorphicEvents" : [ "OrderEvents.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.CANCEL"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /abcd",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "abcd",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/abcd",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.abcd", "click.kamil.examples.statemachine.polymorphic.OrderService.processCustomEvent" ],
"triggerPoint" : {
"event" : "new AbcdEvent()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processCustomEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21,
"polymorphicEvents" : [ "OrderEvents.ABCD" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.ABCD"
} ]
}, {
"entryPoint" : {
"type" : "REST",
"name" : "POST /mystery",
"className" : "click.kamil.examples.statemachine.polymorphic.PolymorphicController",
"methodName" : "mystery",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/PolymorphicController.java",
"metadata" : {
"path" : "/mystery",
"verb" : "POST"
},
"parameters" : [ ]
},
"methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.mystery", "click.kamil.examples.statemachine.polymorphic.OrderService.processCustomEvent" ],
"triggerPoint" : {
"event" : "new MysteryPayload()",
"className" : "click.kamil.examples.statemachine.polymorphic.OrderService",
"methodName" : "processCustomEvent",
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java",
"sourceModule" : null,
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 21,
"polymorphicEvents" : [ "OrderEvents.ABCD", "OrderEvents.PAY" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
"sourceState" : "OrderStates.SUBMITTED",
"targetState" : "OrderStates.PAID",
"event" : "OrderEvents.PAY"
}, {
"sourceState" : "OrderStates.PAID",
"targetState" : "OrderStates.CANCELED",
"event" : "OrderEvents.ABCD"
} ]
} ],
"properties" : {
"default" : {
"spring.application.name" : "statemachinedemo"
}
}
},
"name" : "click.kamil.examples.statemachine.polymorphic.PolymorphicStateMachineConfiguration",
"renderChoicesAsDiamonds" : true,
"startStates" : [ "OrderStates.SUBMITTED" ],
"transitions" : [ {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "OrderStates.SUBMITTED",
"fullIdentifier" : "OrderStates.SUBMITTED"
} ],
"targetStates" : [ {
"rawName" : "OrderStates.PAID",
"fullIdentifier" : "OrderStates.PAID"
} ],
"event" : {
"rawName" : "OrderEvents.PAY",
"fullIdentifier" : "OrderEvents.PAY"
},
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "OrderStates.PAID",
"fullIdentifier" : "OrderStates.PAID"
} ],
"targetStates" : [ {
"rawName" : "OrderStates.FULFILLED",
"fullIdentifier" : "OrderStates.FULFILLED"
} ],
"event" : {
"rawName" : "OrderEvents.FULFILL",
"fullIdentifier" : "OrderEvents.FULFILL"
},
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "OrderStates.SUBMITTED",
"fullIdentifier" : "OrderStates.SUBMITTED"
} ],
"targetStates" : [ {
"rawName" : "OrderStates.CANCELED",
"fullIdentifier" : "OrderStates.CANCELED"
} ],
"event" : {
"rawName" : "OrderEvents.CANCEL",
"fullIdentifier" : "OrderEvents.CANCEL"
},
"guard" : null,
"actions" : [ ],
"order" : null
}, {
"type" : "EXTERNAL",
"sourceStates" : [ {
"rawName" : "OrderStates.PAID",
"fullIdentifier" : "OrderStates.PAID"
} ],
"targetStates" : [ {
"rawName" : "OrderStates.CANCELED",
"fullIdentifier" : "OrderStates.CANCELED"
} ],
"event" : {
"rawName" : "OrderEvents.ABCD",
"fullIdentifier" : "OrderEvents.ABCD"
},
"guard" : null,
"actions" : [ ],
"order" : null
} ],
"endStates" : [ "OrderStates.CANCELED", "OrderStates.FULFILLED" ]
}

View File

@@ -0,0 +1,35 @@
@startuml
!pragma layout smetana
set separator none
hide empty description
hide stereotype
skinparam state {
BackgroundColor white
BorderColor #94a3b8
BorderThickness 1
FontName Inter
FontSize 9
FontStyle bold
RoundCorner 20
Padding 1
}
skinparam shadowing false
skinparam ArrowFontName JetBrains Mono
skinparam ArrowFontSize 8
skinparam ArrowColor #cbd5e1
skinparam ArrowThickness 1
skinparam dpi 110
skinparam svgLinkTarget _self
[*] --> OrderStates.SUBMITTED
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.PAID <<external>> : OrderEvents.PAY
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.FULFILLED <<external>> : OrderEvents.FULFILL
OrderStates.SUBMITTED -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> : OrderEvents.CANCEL
OrderStates.PAID -[#1E90FF,bold]-> OrderStates.CANCELED <<external>> : OrderEvents.ABCD
OrderStates.CANCELED --> [*]
OrderStates.FULFILLED --> [*]
@enduml

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="SUBMITTED">
<state id="SUBMITTED">
<transition target="PAID" event="OrderEvents.PAY"/>
<transition target="CANCELED" event="OrderEvents.CANCEL"/>
</state>
<state id="PAID">
<transition target="FULFILLED" event="OrderEvents.FULFILL"/>
<transition target="CANCELED" event="OrderEvents.ABCD"/>
</state>
<state id="FULFILLED">
</state>
<state id="CANCELED">
</state>
</scxml>

View File

@@ -144,7 +144,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -175,7 +175,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -206,7 +206,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -241,7 +241,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.COMPLETE" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -276,7 +276,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 78,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -311,7 +311,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 25,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.PROCESS" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {
@@ -346,7 +346,7 @@
"stateMachineId" : null,
"sourceState" : null,
"lineNumber" : 50,
"polymorphicEvents" : [ ]
"polymorphicEvents" : [ "OrderEvent.CANCEL" ]
},
"contextMachineId" : null,
"matchedTransitions" : [ {

View File

@@ -1,6 +0,0 @@
[
{
"methodFqn": "ExternalTaxService.calculateTax",
"event": "FINALIZE"
}
]

View File

@@ -1,6 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
.gradle/
.idea/
*.class
*.log
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

Some files were not shown because too many files have changed in this diff Show More