Compare commits
43 Commits
ai-branch-
...
74a4e66a33
| Author | SHA1 | Date | |
|---|---|---|---|
| 74a4e66a33 | |||
| 4511e0e02f | |||
| a7c3b08164 | |||
| 8d66af6d24 | |||
| 642f6f8926 | |||
| 76bb88e065 | |||
| 5c7cf3639b | |||
| 016f0e9b21 | |||
| 22a0d3f5aa | |||
| 7008f162b5 | |||
| 60c666f7a1 | |||
| 8a56e92a65 | |||
| e8543ace38 | |||
| 101f2b5dc9 | |||
| f525f3d3ce | |||
| 5ae233eb75 | |||
| 0bda5a0aeb | |||
| c35c75efa3 | |||
| bd55a66985 | |||
| 0ac70bd3c6 | |||
| b45513c5fb | |||
| 75fd45d983 | |||
| b35effd05c | |||
| f30538e8c9 | |||
| 131ccbeda0 | |||
| 14b311be2b | |||
| 1b690582d6 | |||
| 3ca834fa71 | |||
| c6b4a4be1e | |||
| 24a9be3a4e | |||
| 07d241a060 | |||
| 32de0a51c6 | |||
| cbb00e8a0f | |||
| 6596303b7d | |||
| c903b079d3 | |||
| 23d79d1930 | |||
| 2720296d14 | |||
| b8b180ab3d | |||
| fc267c43c6 | |||
| 968601eefc | |||
| e00f4dca81 | |||
| bf82cc3562 | |||
| 24b67be64b |
@@ -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.
|
||||
|
||||
|
||||
@@ -12,27 +12,49 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
|
||||
return false;
|
||||
}
|
||||
|
||||
String triggerEvent = simplify(triggerPoint.getEvent());
|
||||
String rawTriggerEvent = triggerPoint.getEvent();
|
||||
String smEventRaw = stateMachineEvent.fullIdentifier() != null ? stateMachineEvent.fullIdentifier() : stateMachineEvent.rawName();
|
||||
String smEvent = simplify(smEventRaw);
|
||||
|
||||
boolean isWildcard = isWildcardVariable(triggerEvent);
|
||||
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)) {
|
||||
hasPolyMatch = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
String simplePe = pe;
|
||||
if (pe.contains(".")) {
|
||||
simplePe = pe.substring(pe.lastIndexOf('.') + 1);
|
||||
}
|
||||
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent)) {
|
||||
String simplifiedPe = simplify(simplePe);
|
||||
|
||||
if (simplePe.equals(smEventRaw) || simplePe.equals(smEvent) ||
|
||||
simplePe.equalsIgnoreCase(smEvent) || simplifiedPe.equalsIgnoreCase(smEvent)) {
|
||||
hasPolyMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hasPolyMatch || smEvent.equals(triggerEvent) || (polyEvents.isEmpty() && isWildcard);
|
||||
if (hasPolyMatch) return true;
|
||||
if (polyEvents.isEmpty() && isWildcard) return true;
|
||||
|
||||
if (rawTriggerEvent.contains(".") && smEventRaw.contains(".")) {
|
||||
if (smEventRaw.equals(rawTriggerEvent) || smEventRaw.endsWith("." + rawTriggerEvent)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String triggerEvent = simplify(rawTriggerEvent);
|
||||
return smEvent.equals(triggerEvent);
|
||||
}
|
||||
|
||||
private boolean isWildcardVariable(String eventStr) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class ForwardPathEstimationEnricher implements AnalysisEnricher {
|
||||
|
||||
private final boolean isEnabled;
|
||||
private final PathValueEstimator estimator;
|
||||
|
||||
public ForwardPathEstimationEnricher() {
|
||||
this.isEnabled = Boolean.parseBoolean(System.getProperty("analyzer.forward-path-estimation.enabled", "false"));
|
||||
this.estimator = new SymbolicPathValueEstimator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
if (!isEnabled || result.getMetadata() == null || result.getMetadata().getCallChains() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Running Symbolic Forward Path Analysis for {}", result.getName());
|
||||
|
||||
List<CallChain> chains = result.getMetadata().getCallChains();
|
||||
List<CallChain> updatedChains = chains.stream().map(chain -> {
|
||||
if (chain.getTriggerPoint() == null || chain.getTriggerPoint().getEvent() == null) return chain;
|
||||
|
||||
// If the event looks like a dynamic variable (no dots, not an enum format)
|
||||
if (!chain.getTriggerPoint().getEvent().contains(".")) {
|
||||
List<String> estimatedValues = estimator.estimatePossibleValues(chain, context);
|
||||
if (estimatedValues != null && !estimatedValues.isEmpty()) {
|
||||
log.info("Symbolic Path Analysis estimated values for {} -> {}", chain.getTriggerPoint().getEvent(), estimatedValues);
|
||||
|
||||
List<String> polyEvents = new ArrayList<>();
|
||||
if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
|
||||
polyEvents.addAll(chain.getTriggerPoint().getPolymorphicEvents());
|
||||
}
|
||||
polyEvents.addAll(estimatedValues);
|
||||
|
||||
TriggerPoint newTp = chain.getTriggerPoint().toBuilder()
|
||||
.polymorphicEvents(polyEvents)
|
||||
.build();
|
||||
|
||||
return chain.toBuilder().triggerPoint(newTp).build();
|
||||
}
|
||||
}
|
||||
return chain;
|
||||
}).collect(Collectors.toList());
|
||||
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata updatedMetadata =
|
||||
click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||
.triggers(result.getMetadata().getTriggers())
|
||||
.entryPoints(result.getMetadata().getEntryPoints())
|
||||
.callChains(updatedChains)
|
||||
.properties(result.getMetadata().getProperties())
|
||||
.build();
|
||||
|
||||
result.setMetadata(updatedMetadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PathValueEstimator {
|
||||
List<String> estimatePossibleValues(CallChain chain, CodebaseContext context);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.path;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class SymbolicPathValueEstimator implements PathValueEstimator {
|
||||
|
||||
@Override
|
||||
public List<String> estimatePossibleValues(CallChain chain, CodebaseContext context) {
|
||||
if (chain.getMethodChain() == null || chain.getMethodChain().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<String> collectedConstants = new HashSet<>();
|
||||
|
||||
for (String methodFqn : chain.getMethodChain()) {
|
||||
int lastDot = methodFqn.lastIndexOf('.');
|
||||
if (lastDot == -1) continue;
|
||||
|
||||
String className = methodFqn.substring(0, lastDot);
|
||||
String methodName = methodFqn.substring(lastDot + 1);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) continue;
|
||||
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md == null || md.getBody() == null) continue;
|
||||
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ((node.getName().getIdentifier().equals("equals") || node.getName().getIdentifier().equals("equalsIgnoreCase"))
|
||||
&& node.arguments().size() == 1) {
|
||||
|
||||
Expression caller = node.getExpression();
|
||||
Expression arg = (Expression) node.arguments().get(0);
|
||||
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||
|
||||
String callerVal = resolver.resolve(caller, context);
|
||||
if (callerVal != null && !callerVal.isEmpty()) {
|
||||
addValue(collectedConstants, callerVal);
|
||||
}
|
||||
|
||||
String argVal = resolver.resolve(arg, context);
|
||||
if (argVal != null && !argVal.isEmpty()) {
|
||||
addValue(collectedConstants, argVal);
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(InfixExpression node) {
|
||||
if (node.getOperator() == InfixExpression.Operator.EQUALS) {
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||
String left = resolver.resolve(node.getLeftOperand(), context);
|
||||
if (left != null && !left.isEmpty()) {
|
||||
addValue(collectedConstants, left);
|
||||
}
|
||||
String right = resolver.resolve(node.getRightOperand(), context);
|
||||
if (right != null && !right.isEmpty()) {
|
||||
addValue(collectedConstants, right);
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SwitchStatement node) {
|
||||
for (Object stmt : node.statements()) {
|
||||
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
|
||||
for (Object expr : sc.expressions()) {
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||
String val = resolver.resolve((Expression) expr, context);
|
||||
if (val != null && !val.isEmpty()) {
|
||||
addValue(collectedConstants, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SwitchExpression node) {
|
||||
for (Object stmt : node.statements()) {
|
||||
if (stmt instanceof SwitchCase sc && !sc.isDefault()) {
|
||||
for (Object expr : sc.expressions()) {
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver();
|
||||
String val = resolver.resolve((Expression) expr, context);
|
||||
if (val != null && !val.isEmpty()) {
|
||||
addValue(collectedConstants, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (collectedConstants.isEmpty()) return null;
|
||||
return new ArrayList<>(collectedConstants);
|
||||
}
|
||||
|
||||
private void addValue(Set<String> collectedConstants, String val) {
|
||||
if (val.startsWith("ENUM_SET:")) {
|
||||
for (String eVal : val.substring(9).split(",")) {
|
||||
String parsed = parseEnumSetElement(eVal);
|
||||
if (!collectedConstants.contains(parsed)) collectedConstants.add(parsed);
|
||||
}
|
||||
} else {
|
||||
collectedConstants.add(val);
|
||||
}
|
||||
}
|
||||
|
||||
private String parseEnumSetElement(String element) {
|
||||
if (element.contains(".")) {
|
||||
return element.substring(element.lastIndexOf('.') + 1);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,21 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
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);
|
||||
|
||||
@@ -1,11 +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 final String targetMethod;
|
||||
private final List<String> arguments;
|
||||
private String targetMethod;
|
||||
private List<String> arguments;
|
||||
private String receiver;
|
||||
|
||||
public CallEdge(String targetMethod, List<String> arguments) {
|
||||
this(targetMethod, arguments, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,6 +72,9 @@ 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);
|
||||
}
|
||||
@@ -69,12 +93,19 @@ public class ConstantResolver {
|
||||
|
||||
// Fallback for unresolved AST nodes
|
||||
if (mi.getExpression() instanceof QualifiedName qn) {
|
||||
return qn.getName().getIdentifier();
|
||||
System.out.println("RETURNING qn name: " + qn.getName().getIdentifier()); return qn.getName().getIdentifier();
|
||||
} else if (mi.getExpression() instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
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();
|
||||
@@ -85,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,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;
|
||||
|
||||
@@ -121,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);
|
||||
@@ -180,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);
|
||||
}
|
||||
@@ -225,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PolymorphicEventResolver {
|
||||
|
||||
/**
|
||||
* Attempts to resolve a dynamically passed event variable into a set of
|
||||
* concrete polymorphic event names.
|
||||
*
|
||||
* @param triggerPoint The original trigger point to resolve.
|
||||
* @param resolvedValue The current String expression (e.g., "event.getType()" or "event")
|
||||
* @param path The call path trace leading up to this variable.
|
||||
* @param context The codebase context for deep type resolution.
|
||||
* @return The updated TriggerPoint with polymorphic events set, or the original if unable to resolve.
|
||||
*/
|
||||
TriggerPoint resolvePolymorphicEvents(TriggerPoint triggerPoint, String resolvedValue, List<String> path, CodebaseContext context);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -265,8 +265,26 @@ public class GenericEventDetector {
|
||||
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;
|
||||
@@ -325,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()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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.callGraphEngine = new HeuristicCallGraphEngine(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);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.*;
|
||||
|
||||
public class TypeResolver {
|
||||
private final CodebaseContext context;
|
||||
|
||||
public TypeResolver(CodebaseContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public int getParameterIndex(String methodFqn, String paramName) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return -1;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
for (int i = 0; i < md.parameters().size(); i++) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
|
||||
if (svd.getName().getIdentifier().equals(paramName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public String getParameterType(String methodFqn, int index) {
|
||||
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && index < md.parameters().size()) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(index);
|
||||
return svd.getType().toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isTypeCompatible(String actualType, String expectedType) {
|
||||
if (actualType == null || expectedType == null) return true;
|
||||
if (expectedType.equals("Object") || expectedType.equals("java.lang.Object")) return true;
|
||||
if (actualType.equals("Object") || actualType.equals("java.lang.Object")) return true;
|
||||
if (expectedType.length() == 1 && Character.isUpperCase(expectedType.charAt(0))) return true;
|
||||
if (actualType.length() == 1 && Character.isUpperCase(actualType.charAt(0))) return true;
|
||||
|
||||
String originalExpectedType = expectedType;
|
||||
|
||||
if (actualType.contains("<")) {
|
||||
String rawType = actualType.substring(0, actualType.indexOf('<'));
|
||||
if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) {
|
||||
int start = actualType.indexOf('<');
|
||||
int end = actualType.lastIndexOf('>');
|
||||
if (start >= 0 && end > start) {
|
||||
actualType = actualType.substring(start + 1, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expectedType.contains("<")) {
|
||||
String rawType = expectedType.substring(0, expectedType.indexOf('<'));
|
||||
if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) {
|
||||
int start = expectedType.indexOf('<');
|
||||
int end = expectedType.lastIndexOf('>');
|
||||
if (start >= 0 && end > start) {
|
||||
expectedType = expectedType.substring(start + 1, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actualType.contains("<")) {
|
||||
actualType = actualType.substring(0, actualType.indexOf('<'));
|
||||
}
|
||||
if (expectedType.contains("<")) {
|
||||
expectedType = expectedType.substring(0, expectedType.indexOf('<'));
|
||||
}
|
||||
|
||||
if (actualType.equals(expectedType)) return true;
|
||||
if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true;
|
||||
|
||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType);
|
||||
if (td != null) {
|
||||
if (td instanceof TypeDeclaration typeDecl) {
|
||||
String superFqn = context.getSuperclassFqn(typeDecl);
|
||||
while (superFqn != null) {
|
||||
if (superFqn.contains("<")) {
|
||||
superFqn = superFqn.substring(0, superFqn.indexOf('<'));
|
||||
}
|
||||
if (superFqn.equals(expectedType) || superFqn.endsWith("." + expectedType) || expectedType.endsWith("." + superFqn)) return true;
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
superFqn = superTd != null ? context.getSuperclassFqn(superTd) : null;
|
||||
}
|
||||
}
|
||||
List interfaces = java.util.Collections.emptyList();
|
||||
if (td instanceof TypeDeclaration typeDecl) {
|
||||
interfaces = typeDecl.superInterfaceTypes();
|
||||
} else if (td instanceof EnumDeclaration enumDecl) {
|
||||
interfaces = enumDecl.superInterfaceTypes();
|
||||
} else if (td instanceof RecordDeclaration recordDecl) {
|
||||
interfaces = recordDecl.superInterfaceTypes();
|
||||
}
|
||||
for (Object interfaceType : interfaces) {
|
||||
String itStr = interfaceType.toString();
|
||||
if (itStr.contains("<")) {
|
||||
itStr = itStr.substring(0, itStr.indexOf('<'));
|
||||
}
|
||||
if (itStr.equals(expectedType) || itStr.endsWith("." + expectedType) || expectedType.endsWith("." + itStr)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (originalExpectedType.contains("<")) {
|
||||
int start = originalExpectedType.indexOf('<');
|
||||
int end = originalExpectedType.lastIndexOf('>');
|
||||
if (start >= 0 && end > start) {
|
||||
String sub = originalExpectedType.substring(start + 1, end);
|
||||
for (String part : sub.split(",")) {
|
||||
String typeArg = part.trim();
|
||||
if (isTypeCompatible(actualType, typeArg)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||
|
||||
import org.eclipse.jdt.core.dom.IAnnotationBinding;
|
||||
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
|
||||
import org.eclipse.jdt.core.dom.IVariableBinding;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class InjectionPointAnalyzer {
|
||||
|
||||
private final SpringDependencyResolver resolver;
|
||||
|
||||
public InjectionPointAnalyzer(SpringDependencyResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the Spring bean injected into the given variable.
|
||||
*
|
||||
* @param variableBinding The binding of the field or parameter being injected.
|
||||
* @return The FQN of the resolved concrete bean class, or null if unresolved.
|
||||
*/
|
||||
public String resolveInjectedBeanFqn(IVariableBinding variableBinding) {
|
||||
if (variableBinding == null || variableBinding.getType() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String requiredTypeFqn = variableBinding.getType().getQualifiedName();
|
||||
String injectionName = variableBinding.getName();
|
||||
String qualifier = extractQualifier(variableBinding);
|
||||
|
||||
List<SpringBean> resolvedBeans = resolver.resolve(requiredTypeFqn, qualifier, injectionName);
|
||||
|
||||
if (resolvedBeans != null && resolvedBeans.size() == 1) {
|
||||
return resolvedBeans.get(0).getTypeFqn();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractQualifier(IVariableBinding binding) {
|
||||
String qualifier = getQualifierValue(binding.getAnnotations());
|
||||
if (qualifier != null) {
|
||||
return qualifier;
|
||||
}
|
||||
|
||||
if (binding.isField()) {
|
||||
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||
if (declaringClass != null) {
|
||||
for (org.eclipse.jdt.core.dom.IMethodBinding method : declaringClass.getDeclaredMethods()) {
|
||||
boolean isAutowiredMethod = false;
|
||||
for (IAnnotationBinding ann : method.getAnnotations()) {
|
||||
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Autowired".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||
isAutowiredMethod = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (method.isConstructor() || isAutowiredMethod) {
|
||||
for (int i = 0; i < method.getParameterTypes().length; i++) {
|
||||
org.eclipse.jdt.core.dom.ITypeBinding paramType = method.getParameterTypes()[i];
|
||||
try {
|
||||
IAnnotationBinding[] paramAnns = method.getParameterAnnotations(i);
|
||||
String paramQual = getQualifierValue(paramAnns);
|
||||
if (paramQual != null && paramType.getErasure().isEqualTo(binding.getType().getErasure())) {
|
||||
// For setters, verify it roughly matches the field name or just rely on type.
|
||||
// We will rely on type equality for now as a heuristic.
|
||||
return paramQual;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getQualifierValue(IAnnotationBinding[] annotations) {
|
||||
for (IAnnotationBinding ann : annotations) {
|
||||
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
|
||||
return (String) pair.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class SpringBean {
|
||||
private String typeFqn;
|
||||
|
||||
@Builder.Default
|
||||
private Set<String> assignableTypes = new HashSet<>();
|
||||
|
||||
@Builder.Default
|
||||
private Set<String> beanNames = new HashSet<>();
|
||||
|
||||
@Builder.Default
|
||||
private Set<String> qualifiers = new HashSet<>();
|
||||
|
||||
private boolean isPrimary;
|
||||
private Integer order;
|
||||
private String declaringClassFqn;
|
||||
private String factoryMethodName;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class SpringBeanRegistry {
|
||||
private final List<SpringBean> beans = new ArrayList<>();
|
||||
|
||||
public void addBean(SpringBean bean) {
|
||||
beans.add(bean);
|
||||
}
|
||||
|
||||
public List<SpringBean> getBeans() {
|
||||
return Collections.unmodifiableList(beans);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class SpringContextScanner extends ASTVisitor {
|
||||
private final SpringBeanRegistry registry;
|
||||
|
||||
public SpringContextScanner(SpringBeanRegistry registry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(TypeDeclaration node) {
|
||||
ITypeBinding typeBinding = node.resolveBinding();
|
||||
if (typeBinding == null) return true;
|
||||
|
||||
if (isSpringComponent(typeBinding)) {
|
||||
SpringBean bean = SpringBean.builder()
|
||||
.typeFqn(typeBinding.getQualifiedName())
|
||||
.assignableTypes(getAssignableTypes(typeBinding))
|
||||
.isPrimary(hasAnnotation(typeBinding, "org.springframework.context.annotation.Primary"))
|
||||
.qualifiers(extractQualifiers(typeBinding))
|
||||
.order(extractOrder(typeBinding))
|
||||
.declaringClassFqn(typeBinding.getQualifiedName())
|
||||
.build();
|
||||
|
||||
// Default bean name is uncapitalized simple name
|
||||
String defaultName = Character.toLowerCase(node.getName().getIdentifier().charAt(0)) + node.getName().getIdentifier().substring(1);
|
||||
|
||||
// Check if @Component or @Service specifies a name (e.g., @Service("myService"))
|
||||
String explicitName = extractStereotypeValue(typeBinding);
|
||||
if (explicitName != null && !explicitName.isEmpty()) {
|
||||
bean.getBeanNames().add(explicitName);
|
||||
} else {
|
||||
bean.getBeanNames().add(defaultName);
|
||||
}
|
||||
|
||||
registry.addBean(bean);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
IMethodBinding methodBinding = node.resolveBinding();
|
||||
if (methodBinding == null) return true;
|
||||
|
||||
if (hasAnnotation(methodBinding, "org.springframework.context.annotation.Bean")) {
|
||||
ITypeBinding returnType = methodBinding.getReturnType();
|
||||
if (returnType != null) {
|
||||
Set<String> assignables = getAssignableTypes(returnType);
|
||||
String[] concreteType = new String[] { returnType.getQualifiedName() };
|
||||
|
||||
if (node.getBody() != null) {
|
||||
node.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement ret) {
|
||||
if (ret.getExpression() != null) {
|
||||
ITypeBinding exprType = ret.getExpression().resolveTypeBinding();
|
||||
if (exprType != null && exprType.isClass()) {
|
||||
concreteType[0] = exprType.getQualifiedName();
|
||||
assignables.addAll(getAssignableTypes(exprType));
|
||||
}
|
||||
}
|
||||
return super.visit(ret);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SpringBean bean = SpringBean.builder()
|
||||
.typeFqn(concreteType[0])
|
||||
.assignableTypes(assignables)
|
||||
.isPrimary(hasAnnotation(methodBinding, "org.springframework.context.annotation.Primary"))
|
||||
.qualifiers(extractQualifiers(methodBinding))
|
||||
.order(extractOrder(methodBinding))
|
||||
.declaringClassFqn(methodBinding.getDeclaringClass().getQualifiedName())
|
||||
.factoryMethodName(node.getName().getIdentifier())
|
||||
.build();
|
||||
|
||||
// Default bean name is method name
|
||||
String defaultName = node.getName().getIdentifier();
|
||||
|
||||
// Check if @Bean specifies a name (e.g., @Bean("myBean"))
|
||||
String explicitName = extractBeanName(methodBinding);
|
||||
if (explicitName != null && !explicitName.isEmpty()) {
|
||||
bean.getBeanNames().add(explicitName);
|
||||
} else {
|
||||
bean.getBeanNames().add(defaultName);
|
||||
}
|
||||
|
||||
registry.addBean(bean);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isSpringComponent(ITypeBinding binding) {
|
||||
return hasMetaAnnotation(binding, "org.springframework.stereotype.Component") ||
|
||||
hasMetaAnnotation(binding, "org.springframework.web.bind.annotation.RestController");
|
||||
}
|
||||
|
||||
private boolean hasMetaAnnotation(ITypeBinding binding, String targetFqn) {
|
||||
return checkMetaAnnotation(binding.getAnnotations(), targetFqn, new HashSet<>());
|
||||
}
|
||||
|
||||
private boolean checkMetaAnnotation(IAnnotationBinding[] annotations, String targetFqn, Set<String> visited) {
|
||||
for (IAnnotationBinding ann : annotations) {
|
||||
ITypeBinding annType = ann.getAnnotationType();
|
||||
if (annType != null) {
|
||||
String fqn = annType.getQualifiedName();
|
||||
if (fqn.equals(targetFqn)) return true;
|
||||
|
||||
// Avoid circular meta-annotations (e.g. Documented -> Documented)
|
||||
if (visited.add(fqn)) {
|
||||
if (checkMetaAnnotation(annType.getAnnotations(), targetFqn, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasAnnotation(IBinding binding, String annotationFqn) {
|
||||
IAnnotationBinding[] annotations = binding.getAnnotations();
|
||||
for (IAnnotationBinding ann : annotations) {
|
||||
if (ann.getAnnotationType() != null && annotationFqn.equals(ann.getAnnotationType().getQualifiedName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Set<String> getAssignableTypes(ITypeBinding binding) {
|
||||
Set<String> types = new HashSet<>();
|
||||
collectAssignableTypes(binding, types);
|
||||
return types;
|
||||
}
|
||||
|
||||
private void collectAssignableTypes(ITypeBinding binding, Set<String> types) {
|
||||
if (binding == null) return;
|
||||
|
||||
String qName = binding.getQualifiedName();
|
||||
if (qName != null && !qName.isEmpty()) {
|
||||
types.add(qName);
|
||||
}
|
||||
|
||||
ITypeBinding erasure = binding.getErasure();
|
||||
if (erasure != null) {
|
||||
String erasureName = erasure.getQualifiedName();
|
||||
if (erasureName != null && !erasureName.isEmpty()) {
|
||||
types.add(erasureName);
|
||||
}
|
||||
}
|
||||
|
||||
collectAssignableTypes(binding.getSuperclass(), types);
|
||||
if (binding.getInterfaces() != null) {
|
||||
for (ITypeBinding iface : binding.getInterfaces()) {
|
||||
collectAssignableTypes(iface, types);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> extractQualifiers(IBinding binding) {
|
||||
Set<String> qualifiers = new HashSet<>();
|
||||
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
||||
if (ann.getAnnotationType() != null && "org.springframework.beans.factory.annotation.Qualifier".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
|
||||
qualifiers.add((String) pair.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return qualifiers;
|
||||
}
|
||||
|
||||
private Integer extractOrder(IBinding binding) {
|
||||
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
||||
if (ann.getAnnotationType() != null && "org.springframework.core.annotation.Order".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||
if ("value".equals(pair.getName())) {
|
||||
Object val = pair.getValue();
|
||||
if (val instanceof Integer) {
|
||||
return (Integer) val;
|
||||
} else if (val instanceof IVariableBinding) {
|
||||
Object constantValue = ((IVariableBinding) val).getConstantValue();
|
||||
if (constantValue instanceof Integer) {
|
||||
return (Integer) constantValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Integer.MAX_VALUE; // Spring's default for @Order()
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractStereotypeValue(ITypeBinding binding) {
|
||||
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
||||
// Check if annotation itself is meta-annotated with @Component
|
||||
if (checkMetaAnnotation(new IAnnotationBinding[]{ann}, "org.springframework.stereotype.Component", new HashSet<>()) ||
|
||||
"org.springframework.stereotype.Component".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||
if ("value".equals(pair.getName()) && pair.getValue() instanceof String) {
|
||||
return (String) pair.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractBeanName(IMethodBinding binding) {
|
||||
for (IAnnotationBinding ann : binding.getAnnotations()) {
|
||||
if ("org.springframework.context.annotation.Bean".equals(ann.getAnnotationType().getQualifiedName())) {
|
||||
for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
|
||||
if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof String) {
|
||||
return (String) pair.getValue();
|
||||
} else if (("value".equals(pair.getName()) || "name".equals(pair.getName())) && pair.getValue() instanceof Object[]) {
|
||||
Object[] vals = (Object[]) pair.getValue();
|
||||
if (vals.length > 0 && vals[0] instanceof String) {
|
||||
return (String) vals[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SpringDependencyResolver {
|
||||
private final SpringBeanRegistry registry;
|
||||
|
||||
public SpringDependencyResolver(SpringBeanRegistry registry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the exact SpringBean that would be injected for a given injection point.
|
||||
*
|
||||
* @param requiredTypeFqn The exact FQN of the type requested at the injection point.
|
||||
* @param qualifier The value of the @Qualifier annotation on the injection point (if any).
|
||||
* @param injectionName The name of the field or parameter being injected (used as fallback).
|
||||
* @return A list of resolved beans. Usually 1. If 0, unresolved. If > 1, ambiguous.
|
||||
*/
|
||||
public List<SpringBean> resolve(String requiredTypeFqn, String qualifier, String injectionName) {
|
||||
if (requiredTypeFqn == null) return new ArrayList<>();
|
||||
|
||||
// 1. Filter by Type (Exact FQN or Assignable Type)
|
||||
System.out.println("RESOLVING " + requiredTypeFqn + " qual=" + qualifier + " injectionName=" + injectionName);
|
||||
List<SpringBean> candidates = registry.getBeans().stream()
|
||||
.filter(bean -> requiredTypeFqn.equals(bean.getTypeFqn()) || bean.getAssignableTypes().contains(requiredTypeFqn))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
System.out.println("CANDIDATES AFTER TYPE: " + candidates.stream().map(c -> c.getTypeFqn() + " (primary=" + c.isPrimary() + " names=" + c.getBeanNames() + " qual=" + c.getQualifiers() + ")").collect(Collectors.toList()));
|
||||
if (candidates.isEmpty() || candidates.size() == 1) {
|
||||
System.out.println("RETURNING: " + candidates.size());
|
||||
return candidates;
|
||||
}
|
||||
|
||||
// 2. Filter by Qualifier (if provided)
|
||||
if (qualifier != null && !qualifier.isEmpty()) {
|
||||
List<SpringBean> qualifiedCandidates = candidates.stream()
|
||||
.filter(bean -> bean.getQualifiers().contains(qualifier) || bean.getBeanNames().contains(qualifier))
|
||||
.collect(Collectors.toList());
|
||||
if (qualifiedCandidates.size() == 1) {
|
||||
return qualifiedCandidates;
|
||||
} else if (!qualifiedCandidates.isEmpty()) {
|
||||
candidates = qualifiedCandidates;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Filter by @Primary
|
||||
List<SpringBean> primaryCandidates = candidates.stream()
|
||||
.filter(SpringBean::isPrimary)
|
||||
.collect(Collectors.toList());
|
||||
if (primaryCandidates.size() == 1) {
|
||||
return primaryCandidates;
|
||||
}
|
||||
|
||||
// 4. Fallback to Injection Name (field name or parameter name)
|
||||
if (injectionName != null && !injectionName.isEmpty()) {
|
||||
List<SpringBean> namedCandidates = candidates.stream()
|
||||
.filter(bean -> bean.getBeanNames().contains(injectionName))
|
||||
.collect(Collectors.toList());
|
||||
if (namedCandidates.size() == 1) {
|
||||
return namedCandidates;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Fallback to @Order (lowest order wins)
|
||||
Integer minOrder = candidates.stream()
|
||||
.map(SpringBean::getOrder)
|
||||
.filter(o -> o != null)
|
||||
.min(Integer::compareTo)
|
||||
.orElse(null);
|
||||
|
||||
if (minOrder != null) {
|
||||
List<SpringBean> orderedCandidates = candidates.stream()
|
||||
.filter(bean -> bean.getOrder() != null && bean.getOrder().equals(minOrder))
|
||||
.collect(Collectors.toList());
|
||||
if (!orderedCandidates.isEmpty()) {
|
||||
candidates = orderedCandidates;
|
||||
}
|
||||
}
|
||||
|
||||
// Return whatever candidates remain (could be ambiguous)
|
||||
System.out.println("RETURNING: " + candidates.size());
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import click.kamil.springstatemachineexporter.model.TransitionType;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class StateMachineAdapterParser {
|
||||
private final CodebaseContext context;
|
||||
|
||||
private enum AnalysisGoal {TRANSITIONS, STATES}
|
||||
|
||||
public record AdapterStates(Set<String> initialStates, Set<String> endStates) {}
|
||||
|
||||
public record AdapterConfig(
|
||||
List<Transition> transitions,
|
||||
Set<String> initialStates,
|
||||
Set<String> endStates
|
||||
) {}
|
||||
|
||||
public StateMachineAdapterParser(CodebaseContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public AdapterConfig parse(TypeDeclaration startClass) {
|
||||
List<Transition> transitions = parseTransitions(startClass);
|
||||
AdapterStates states = parseStates(startClass);
|
||||
return new AdapterConfig(transitions, states.initialStates(), states.endStates());
|
||||
}
|
||||
|
||||
public List<Transition> parseTransitions(TypeDeclaration startClass) {
|
||||
List<Transition> allTransitions = new ArrayList<>();
|
||||
Set<MethodVisit> visitedMethods = new HashSet<>();
|
||||
aggregateTransitionsRecursive(startClass, startClass, allTransitions, new HashSet<>(), visitedMethods, new HashMap<>());
|
||||
return allTransitions;
|
||||
}
|
||||
|
||||
public AdapterStates parseStates(TypeDeclaration startClass) {
|
||||
Set<String> initialStates = new HashSet<>();
|
||||
Set<String> endStates = new HashSet<>();
|
||||
Set<MethodVisit> visitedMethods = new HashSet<>();
|
||||
aggregateStatesRecursive(startClass, startClass, initialStates, endStates, new HashSet<>(), visitedMethods, new HashMap<>());
|
||||
return new AdapterStates(initialStates, endStates);
|
||||
}
|
||||
|
||||
private void aggregateTransitionsRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, List<Transition> allTransitions, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
if (currentTd == null)
|
||||
return;
|
||||
String fqn = context.getFqn(currentTd);
|
||||
if (visitedClasses.contains(fqn))
|
||||
return;
|
||||
visitedClasses.add(fqn);
|
||||
|
||||
for (MethodDeclaration method : currentTd.getMethods()) {
|
||||
if (isConfigureTransitionsMethod(method)) {
|
||||
allTransitions.addAll(parseTransitionsWithMethodCalls(method, searchStartClass, visitedMethods, argsMap));
|
||||
}
|
||||
}
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
||||
|
||||
// Hierarchy - Superclass
|
||||
Type superclassType = currentTd.getSuperclassType();
|
||||
if (superclassType != null) {
|
||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
||||
if (superclassTd != null) {
|
||||
aggregateTransitionsRecursive(superclassTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||
}
|
||||
}
|
||||
|
||||
// Hierarchy - Interfaces
|
||||
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
||||
if (interfaceTd != null) {
|
||||
aggregateTransitionsRecursive(interfaceTd, searchStartClass, allTransitions, visitedClasses, visitedMethods, argsMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record MethodVisit(MethodDeclaration method, Map<String, Object> args) {
|
||||
}
|
||||
|
||||
private List<Transition> parseTransitionsWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
||||
if (visitedMethods.contains(visit)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
visitedMethods.add(visit);
|
||||
|
||||
Block body = method.getBody();
|
||||
if (body == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return parseTransitionsInBlock(body, searchStartClass, visitedMethods, argsMap);
|
||||
}
|
||||
|
||||
private List<Transition> parseTransitionsInExpression(Expression expr, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
List<Transition> transitions = new ArrayList<>();
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
if (isTransitionChainEntry(mi)) {
|
||||
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), context));
|
||||
} else if (isTransitionChainContinuation(mi, argsMap)) {
|
||||
transitions.addAll(AstTransitionParser.parseTransitionsFromExpression(mi, convertToExpressionMap(argsMap), true, context));
|
||||
} else if (isForEach(mi)) {
|
||||
transitions.addAll(parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||
} else {
|
||||
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
||||
Expression lambdaReceiver = mi.getExpression();
|
||||
if (lambdaReceiver instanceof SimpleName sn) {
|
||||
Object resolved = argsMap.get(sn.getIdentifier());
|
||||
if (resolved instanceof LambdaExpression lambda) {
|
||||
transitions.addAll(parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||
if (calledMethod == null) {
|
||||
// Try to resolve method in another class if it's a static call or on a known type
|
||||
Expression miReceiver = mi.getExpression();
|
||||
if (miReceiver instanceof SimpleName sn) {
|
||||
TypeDeclaration otherTd = context.getTypeDeclaration(sn.getIdentifier(), (CompilationUnit) searchStartClass.getRoot());
|
||||
if (otherTd != null) {
|
||||
calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), otherTd);
|
||||
if (calledMethod != null) {
|
||||
// For calls to other classes, we switch the "searchStartClass" to that class
|
||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, otherTd, visitedMethods, nextArgsMap));
|
||||
return transitions; // Handled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (calledMethod != null) {
|
||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||
transitions.addAll(parseTransitionsWithMethodCalls(calledMethod, searchStartClass, visitedMethods, nextArgsMap));
|
||||
}
|
||||
}
|
||||
}
|
||||
return transitions;
|
||||
}
|
||||
|
||||
private boolean isForEach(MethodInvocation mi) {
|
||||
return mi.getName().getIdentifier().equals("forEach");
|
||||
}
|
||||
|
||||
private List<Transition> parseLambdaInvocation(LambdaExpression lambda, List<?> arguments, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||
List<?> params = lambda.parameters();
|
||||
for (int i = 0; i < params.size() && i < arguments.size(); i++) {
|
||||
Object p = params.get(i);
|
||||
String paramName = "";
|
||||
if (p instanceof VariableDeclaration vp)
|
||||
paramName = vp.getName().getIdentifier();
|
||||
if (!paramName.isEmpty()) {
|
||||
lambdaArgsMap.put(paramName, resolveExpression((Expression) arguments.get(i), argsMap));
|
||||
}
|
||||
}
|
||||
|
||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||
if (lambda.getBody() instanceof Block block)
|
||||
return parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap);
|
||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||
return parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap);
|
||||
} else {
|
||||
if (lambda.getBody() instanceof Block block)
|
||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<Transition> parseForEach(MethodInvocation forEachMi, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||
List<Transition> transitions = new ArrayList<>();
|
||||
Expression receiver = forEachMi.getExpression();
|
||||
List<Expression> elements = unrollCollection(receiver, argsMap);
|
||||
if (elements.isEmpty() || forEachMi.arguments().isEmpty())
|
||||
return transitions;
|
||||
|
||||
Object lambdaObj = forEachMi.arguments().get(0);
|
||||
if (lambdaObj instanceof LambdaExpression lambda) {
|
||||
String paramName = getLambdaParamName(lambda);
|
||||
for (Expression element : elements) {
|
||||
Map<String, Object> lambdaArgsMap = new HashMap<>(argsMap);
|
||||
if (!paramName.isEmpty())
|
||||
lambdaArgsMap.put(paramName, element);
|
||||
|
||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||
if (lambda.getBody() instanceof Block block)
|
||||
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, lambdaArgsMap));
|
||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||
transitions.addAll(parseTransitionsInExpression(lambdaExpr, searchStartClass, visitedMethods, lambdaArgsMap));
|
||||
} else {
|
||||
if (lambda.getBody() instanceof Block block)
|
||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||
else if (lambda.getBody() instanceof Expression lambdaExpr)
|
||||
parseStatesInExpression(lambdaExpr, searchStartClass, initialStates, endStates, visitedMethods, lambdaArgsMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
return transitions;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Transition> parseEnhancedForLoop(EnhancedForStatement efs, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap, AnalysisGoal goal, Set<String> initialStates, Set<String> endStates) {
|
||||
List<Transition> transitions = new ArrayList<>();
|
||||
List<Expression> elements = unrollCollection(efs.getExpression(), argsMap);
|
||||
String paramName = efs.getParameter().getName().getIdentifier();
|
||||
|
||||
for (Expression element : elements) {
|
||||
Map<String, Object> loopArgsMap = new HashMap<>(argsMap);
|
||||
loopArgsMap.put(paramName, element);
|
||||
|
||||
if (goal == AnalysisGoal.TRANSITIONS) {
|
||||
if (efs.getBody() instanceof Block block)
|
||||
transitions.addAll(parseTransitionsInBlock(block, searchStartClass, visitedMethods, loopArgsMap));
|
||||
else if (efs.getBody() instanceof ExpressionStatement es)
|
||||
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, loopArgsMap));
|
||||
} else {
|
||||
if (efs.getBody() instanceof Block block)
|
||||
parseStatesInBlock(block, searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
||||
else if (efs.getBody() instanceof ExpressionStatement es)
|
||||
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, loopArgsMap);
|
||||
}
|
||||
}
|
||||
return transitions;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Expression> unrollCollection(Expression expr, Map<String, Object> argsMap) {
|
||||
Set<Expression> visited = new HashSet<>();
|
||||
Expression current = expr;
|
||||
while (current != null) {
|
||||
if (!visited.add(current))
|
||||
break;
|
||||
|
||||
if (current instanceof MethodInvocation mi) {
|
||||
String name = mi.getName().getIdentifier();
|
||||
Expression receiver = mi.getExpression();
|
||||
if ((name.equals("of") && receiver != null && (receiver.toString().equals("Stream") || receiver.toString().equals("List")))
|
||||
|| (name.equals("asList") && receiver != null && receiver.toString().equals("Arrays"))) {
|
||||
if (mi.arguments().size() == 1) {
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
if (arg instanceof SimpleName sn) {
|
||||
Object resolved = argsMap.get(sn.getIdentifier());
|
||||
if (resolved instanceof List list)
|
||||
return (List<Expression>) list;
|
||||
}
|
||||
}
|
||||
return (List<Expression>) mi.arguments();
|
||||
}
|
||||
}
|
||||
if (current instanceof SimpleName sn) {
|
||||
Object resolved = argsMap.get(sn.getIdentifier());
|
||||
if (resolved instanceof List list)
|
||||
return (List<Expression>) list;
|
||||
if (resolved instanceof Expression e) {
|
||||
current = e;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (current instanceof ArrayInitializer ai)
|
||||
return (List<Expression>) ai.expressions();
|
||||
break;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private String getLambdaParamName(LambdaExpression lambda) {
|
||||
if (lambda.parameters().size() == 1) {
|
||||
Object p = lambda.parameters().get(0);
|
||||
if (p instanceof VariableDeclaration vp)
|
||||
return vp.getName().getIdentifier();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private List<Transition> parseTransitionsInBlock(Block block, TypeDeclaration searchStartClass, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
List<Transition> transitions = new ArrayList<>();
|
||||
for (Object stmt : block.statements()) {
|
||||
if (stmt instanceof ExpressionStatement es)
|
||||
transitions.addAll(parseTransitionsInExpression(es.getExpression(), searchStartClass, visitedMethods, argsMap));
|
||||
else if (stmt instanceof VariableDeclarationStatement vds) {
|
||||
for (Object fragmentObj : vds.fragments()) {
|
||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||
Expression initializer = fragment.getInitializer();
|
||||
if (initializer != null) {
|
||||
Object resolved = resolve(initializer, argsMap);
|
||||
if (resolved != null)
|
||||
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
||||
transitions.addAll(parseTransitionsInExpression(initializer, searchStartClass, visitedMethods, argsMap));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (stmt instanceof EnhancedForStatement efs)
|
||||
transitions.addAll(parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.TRANSITIONS, null, null));
|
||||
else if (stmt instanceof TryStatement ts)
|
||||
transitions.addAll(parseTransitionsInBlock(ts.getBody(), searchStartClass, visitedMethods, argsMap));
|
||||
}
|
||||
return transitions;
|
||||
}
|
||||
|
||||
private void parseStatesInBlock(Block block, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
for (Object stmt : block.statements()) {
|
||||
if (stmt instanceof ExpressionStatement es)
|
||||
parseStatesInExpression(es.getExpression(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||
else if (stmt instanceof VariableDeclarationStatement vds) {
|
||||
for (Object fragmentObj : vds.fragments()) {
|
||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||
Expression initializer = fragment.getInitializer();
|
||||
if (initializer != null) {
|
||||
Object resolved = resolve(initializer, argsMap);
|
||||
if (resolved != null)
|
||||
argsMap.put(fragment.getName().getIdentifier(), resolved);
|
||||
parseStatesInExpression(initializer, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (stmt instanceof EnhancedForStatement efs)
|
||||
parseEnhancedForLoop(efs, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||
else if (stmt instanceof TryStatement ts)
|
||||
parseStatesInBlock(ts.getBody(), searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseStatesInExpression(Expression expr, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
if (isWithStatesChain(mi, argsMap))
|
||||
parseStateChain(mi, initialStates, endStates, argsMap);
|
||||
else if (isForEach(mi))
|
||||
parseForEach(mi, searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||
else {
|
||||
// Check if it's a call on a lambda parameter or a variable that resolved to a lambda
|
||||
Expression receiver = mi.getExpression();
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
Object resolved = argsMap.get(sn.getIdentifier());
|
||||
if (resolved instanceof LambdaExpression lambda) {
|
||||
parseLambdaInvocation(lambda, mi.arguments(), searchStartClass, visitedMethods, argsMap, AnalysisGoal.STATES, initialStates, endStates);
|
||||
}
|
||||
}
|
||||
|
||||
MethodDeclaration calledMethod = findMethodInHierarchy(mi.getName().getIdentifier(), searchStartClass);
|
||||
if (calledMethod != null) {
|
||||
Map<String, Object> nextArgsMap = buildArgsMap(calledMethod, mi, argsMap);
|
||||
parseStatesWithMethodCalls(calledMethod, searchStartClass, initialStates, endStates, visitedMethods, nextArgsMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildArgsMap(MethodDeclaration method, MethodInvocation call, Map<String, Object> currentArgsMap) {
|
||||
Map<String, Object> map = new HashMap<>(currentArgsMap);
|
||||
List<?> params = method.parameters();
|
||||
List<?> args = call.arguments();
|
||||
|
||||
for (int i = 0; i < params.size(); i++) {
|
||||
SingleVariableDeclaration param = (SingleVariableDeclaration) params.get(i);
|
||||
String paramName = param.getName().getIdentifier();
|
||||
if (param.isVarargs() && i < params.size()) {
|
||||
List<Expression> varargList = new ArrayList<>();
|
||||
for (int j = i; j < args.size(); j++) {
|
||||
Object resolved = resolve((Expression) args.get(j), currentArgsMap);
|
||||
if (resolved instanceof List<?> list) {
|
||||
for (Object item : list) {
|
||||
if (item instanceof Expression e)
|
||||
varargList.add(e);
|
||||
}
|
||||
} else if (resolved instanceof Expression e) {
|
||||
varargList.add(e);
|
||||
}
|
||||
}
|
||||
map.put(paramName, varargList);
|
||||
} else if (i < args.size()) {
|
||||
map.put(paramName, resolve((Expression) args.get(i), currentArgsMap));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private Object resolve(Expression expr, Map<String, Object> argsMap) {
|
||||
if (expr instanceof NullLiteral)
|
||||
return null;
|
||||
Set<Expression> visited = new HashSet<>();
|
||||
Object current = expr;
|
||||
while (current instanceof SimpleName sn) {
|
||||
if (!visited.add((SimpleName) current))
|
||||
break;
|
||||
Object resolved = argsMap.get(sn.getIdentifier());
|
||||
if (resolved == null || resolved == current)
|
||||
break;
|
||||
current = resolved;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private Expression resolveExpression(Expression expr, Map<String, Object> argsMap) {
|
||||
Object resolved = resolve(expr, argsMap);
|
||||
if (resolved instanceof Expression e)
|
||||
return e;
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Map<String, Expression> convertToExpressionMap(Map<String, Object> argsMap) {
|
||||
Map<String, Expression> map = new HashMap<>();
|
||||
for (Map.Entry<String, Object> entry : argsMap.entrySet()) {
|
||||
if (entry.getValue() instanceof Expression e)
|
||||
map.put(entry.getKey(), e);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private boolean isTransitionChainEntry(MethodInvocation mi) {
|
||||
Expression current = mi;
|
||||
while (current instanceof MethodInvocation call) {
|
||||
String name = call.getName().getIdentifier();
|
||||
if (TransitionType.fromMethodName(name).isPresent())
|
||||
return true;
|
||||
current = call.getExpression();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isTransitionChainContinuation(MethodInvocation mi, Map<String, Object> argsMap) {
|
||||
Expression current = mi;
|
||||
Set<Expression> visited = new HashSet<>();
|
||||
while (current instanceof MethodInvocation call) {
|
||||
if (!visited.add(current))
|
||||
break;
|
||||
String name = call.getName().getIdentifier();
|
||||
if (Set.of("and", "source", "target", "event", "first", "then", "last").contains(name)) {
|
||||
Expression receiver = call.getExpression();
|
||||
if (receiver instanceof MethodInvocation next) {
|
||||
current = next;
|
||||
continue;
|
||||
}
|
||||
Object resolved = resolve(receiver, argsMap);
|
||||
if (resolved instanceof MethodInvocation next) {
|
||||
current = next;
|
||||
continue;
|
||||
}
|
||||
return resolved == null; // null means it's a parameter we track
|
||||
}
|
||||
if (TransitionType.fromMethodName(name).isPresent())
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td) {
|
||||
return findMethodInHierarchy(methodName, td, new HashSet<>());
|
||||
}
|
||||
|
||||
private MethodDeclaration findMethodInHierarchy(String methodName, TypeDeclaration td, Set<String> visited) {
|
||||
if (td == null)
|
||||
return null;
|
||||
|
||||
String fqn = context.getFqn(td);
|
||||
if (visited.contains(fqn))
|
||||
return null;
|
||||
visited.add(fqn);
|
||||
|
||||
for (MethodDeclaration method : td.getMethods())
|
||||
if (method.getName().getIdentifier().equals(methodName))
|
||||
return method;
|
||||
|
||||
Type superclassType = td.getSuperclassType();
|
||||
if (superclassType != null) {
|
||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), (CompilationUnit) td.getRoot());
|
||||
return findMethodInHierarchy(methodName, superclassTd, visited);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isRelevantMethod(MethodDeclaration method) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private MethodDeclaration findConfigureTransitionsMethod(TypeDeclaration td) {
|
||||
for (MethodDeclaration method : td.getMethods())
|
||||
if (isConfigureTransitionsMethod(method))
|
||||
return method;
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isConfigureTransitionsMethod(MethodDeclaration method) {
|
||||
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
||||
return false;
|
||||
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineTransitionConfigurer");
|
||||
}
|
||||
|
||||
private void aggregateStatesRecursive(TypeDeclaration currentTd, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<String> visitedClasses, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
if (currentTd == null)
|
||||
return;
|
||||
String fqn = context.getFqn(currentTd);
|
||||
if (visitedClasses.contains(fqn))
|
||||
return;
|
||||
visitedClasses.add(fqn);
|
||||
|
||||
for (MethodDeclaration method : currentTd.getMethods()) {
|
||||
if (isConfigureStatesMethod(method)) {
|
||||
parseStatesWithMethodCalls(method, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||
}
|
||||
}
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) currentTd.getRoot();
|
||||
|
||||
// Hierarchy - Superclass
|
||||
Type superclassType = currentTd.getSuperclassType();
|
||||
if (superclassType != null) {
|
||||
TypeDeclaration superclassTd = context.getTypeDeclaration(getSimpleName(superclassType), cu);
|
||||
if (superclassTd != null)
|
||||
aggregateStatesRecursive(superclassTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||
}
|
||||
|
||||
// Hierarchy - Interfaces
|
||||
for (Object interfaceTypeObj : currentTd.superInterfaceTypes()) {
|
||||
if (interfaceTypeObj instanceof Type interfaceType) {
|
||||
TypeDeclaration interfaceTd = context.getTypeDeclaration(getSimpleName(interfaceType), cu);
|
||||
if (interfaceTd != null)
|
||||
aggregateStatesRecursive(interfaceTd, searchStartClass, initialStates, endStates, visitedClasses, visitedMethods, argsMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseStatesWithMethodCalls(MethodDeclaration method, TypeDeclaration searchStartClass, Set<String> initialStates, Set<String> endStates, Set<MethodVisit> visitedMethods, Map<String, Object> argsMap) {
|
||||
MethodVisit visit = new MethodVisit(method, new HashMap<>(argsMap));
|
||||
if (visitedMethods.contains(visit))
|
||||
return;
|
||||
visitedMethods.add(visit);
|
||||
Block body = method.getBody();
|
||||
if (body == null)
|
||||
return;
|
||||
|
||||
parseStatesInBlock(body, searchStartClass, initialStates, endStates, visitedMethods, argsMap);
|
||||
}
|
||||
|
||||
private boolean isWithStatesChain(MethodInvocation mi, Map<String, Object> argsMap) {
|
||||
Expression current = mi;
|
||||
Set<Expression> visited = new HashSet<>();
|
||||
while (current instanceof MethodInvocation call) {
|
||||
if (!visited.add(current))
|
||||
break;
|
||||
if (call.getName().getIdentifier().equals("withStates"))
|
||||
return true;
|
||||
Expression receiver = call.getExpression();
|
||||
if (receiver instanceof MethodInvocation next) {
|
||||
current = next;
|
||||
continue;
|
||||
}
|
||||
Object resolved = resolve(receiver, argsMap);
|
||||
if (resolved instanceof MethodInvocation next) {
|
||||
current = next;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void parseStateChain(MethodInvocation mi, Set<String> initialStates, Set<String> endStates, Map<String, Object> argsMap) {
|
||||
List<MethodInvocation> chain = new ArrayList<>();
|
||||
Expression current = mi;
|
||||
while (current instanceof MethodInvocation m) {
|
||||
chain.addFirst(m);
|
||||
current = m.getExpression();
|
||||
}
|
||||
CompilationUnit cu = (CompilationUnit) mi.getRoot();
|
||||
for (MethodInvocation call : chain) {
|
||||
String methodName = call.getName().getIdentifier();
|
||||
if (call.arguments().isEmpty())
|
||||
continue;
|
||||
Expression arg = resolveExpression((Expression) call.arguments().get(0), argsMap);
|
||||
if ("initial".equals(methodName)) {
|
||||
if (!isInsideRegionOrFork(call)) {
|
||||
initialStates.add(context.resolveState(arg, cu).toString());
|
||||
}
|
||||
} else if ("end".equals(methodName)) {
|
||||
endStates.add(context.resolveState(arg, cu).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInsideRegionOrFork(MethodInvocation mi) {
|
||||
Expression expr = mi.getExpression();
|
||||
while (expr instanceof MethodInvocation parent) {
|
||||
if (List.of("fork", "region").contains(parent.getName().getIdentifier()))
|
||||
return true;
|
||||
expr = parent.getExpression();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private MethodDeclaration findConfigureStatesMethod(TypeDeclaration td) {
|
||||
for (MethodDeclaration method : td.getMethods())
|
||||
if (isConfigureStatesMethod(method))
|
||||
return method;
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isConfigureStatesMethod(MethodDeclaration method) {
|
||||
if (!method.getName().getIdentifier().equals("configure") || method.parameters().isEmpty())
|
||||
return false;
|
||||
return ((SingleVariableDeclaration) method.parameters().get(0)).getType().toString().contains("StateMachineStateConfigurer");
|
||||
}
|
||||
|
||||
private String getSimpleName(Type type) {
|
||||
if (type.isSimpleType())
|
||||
return ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
if (type.isParameterizedType())
|
||||
return getSimpleName(((ParameterizedType) type).getType());
|
||||
return type.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,632 +1,37 @@
|
||||
package click.kamil.springstatemachineexporter.ast.app;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,62 @@ 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) {
|
||||
String fqn = typeBinding.getErasure().getQualifiedName();
|
||||
if (fqn.startsWith("java.util.stream.") ||
|
||||
fqn.equals("java.lang.Iterable") ||
|
||||
fqn.equals("java.util.Collection") ||
|
||||
fqn.equals("java.util.List") ||
|
||||
fqn.equals("java.util.Set") ||
|
||||
fqn.equals("java.util.Map") ||
|
||||
fqn.equals("java.util.Iterator") ||
|
||||
fqn.equals("java.util.Optional") ||
|
||||
fqn.equals("reactor.core.publisher.Flux") ||
|
||||
fqn.equals("reactor.core.publisher.Mono")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (org.eclipse.jdt.core.dom.ITypeBinding intf : typeBinding.getInterfaces()) {
|
||||
if (isCollectionOrStreamType(intf)) return true;
|
||||
}
|
||||
|
||||
if (typeBinding.getSuperclass() != null) {
|
||||
return isCollectionOrStreamType(typeBinding.getSuperclass());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.Set;
|
||||
|
||||
public final class BindingResolver {
|
||||
|
||||
private BindingResolver() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the type binding for a given AST Type node.
|
||||
*/
|
||||
public static ITypeBinding resolveType(Type type) {
|
||||
if (type == null) return null;
|
||||
return type.resolveBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the type binding for a given AST Expression node.
|
||||
*/
|
||||
public static ITypeBinding resolveType(Expression expression) {
|
||||
if (expression == null) return null;
|
||||
return expression.resolveTypeBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the method binding for a MethodInvocation.
|
||||
*/
|
||||
public static IMethodBinding resolveMethod(MethodInvocation invocation) {
|
||||
if (invocation == null) return null;
|
||||
return invocation.resolveMethodBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the constructor binding for a ClassInstanceCreation.
|
||||
*/
|
||||
public static IMethodBinding resolveConstructor(ClassInstanceCreation creation) {
|
||||
if (creation == null) return null;
|
||||
return creation.resolveConstructorBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the constructor binding for a ConstructorInvocation (e.g. this(...)).
|
||||
*/
|
||||
public static IMethodBinding resolveConstructor(ConstructorInvocation invocation) {
|
||||
if (invocation == null) return null;
|
||||
return invocation.resolveConstructorBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the constructor binding for a SuperConstructorInvocation (e.g. super(...)).
|
||||
*/
|
||||
public static IMethodBinding resolveConstructor(SuperConstructorInvocation invocation) {
|
||||
if (invocation == null) return null;
|
||||
return invocation.resolveConstructorBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the type binding of a TypeDeclaration.
|
||||
*/
|
||||
public static ITypeBinding resolveType(TypeDeclaration td) {
|
||||
if (td == null) return null;
|
||||
return td.resolveBinding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fully qualified name of a type binding, safely handling nulls and erasure.
|
||||
*/
|
||||
public static String getFullyQualifiedName(ITypeBinding binding) {
|
||||
if (binding == null) return null;
|
||||
return binding.getErasure().getQualifiedName();
|
||||
}
|
||||
|
||||
public static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn) {
|
||||
return isSubtypeOf(childBinding, targetFqn, new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
private static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn, Set<String> visited) {
|
||||
if (childBinding == null || targetFqn == null) return false;
|
||||
|
||||
String key = childBinding.getKey();
|
||||
if (key != null && !visited.add(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Direct match
|
||||
if (targetFqn.equals(childBinding.getErasure().getQualifiedName())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check interfaces
|
||||
for (ITypeBinding intf : childBinding.getInterfaces()) {
|
||||
if (isSubtypeOf(intf, targetFqn, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check superclass
|
||||
ITypeBinding superclass = childBinding.getSuperclass();
|
||||
if (superclass != null) {
|
||||
return isSubtypeOf(superclass, targetFqn, visited);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.*;
|
||||
|
||||
public class CfgBuilder {
|
||||
|
||||
public static ControlFlowGraph build(MethodDeclaration method) {
|
||||
ControlFlowGraph cfg = new ControlFlowGraph(method);
|
||||
if (method.getBody() == null) {
|
||||
cfg.getEntryNode().addSuccessor(cfg.getExitNode());
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Build CFG recursively
|
||||
List<ControlFlowGraph.CfgNode> exits = buildFlow(method.getBody(), List.of(cfg.getEntryNode()), cfg);
|
||||
for (ControlFlowGraph.CfgNode exit : exits) {
|
||||
exit.addSuccessor(cfg.getExitNode());
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private static List<ControlFlowGraph.CfgNode> buildFlow(
|
||||
ASTNode node,
|
||||
List<ControlFlowGraph.CfgNode> sources,
|
||||
ControlFlowGraph cfg) {
|
||||
|
||||
if (node == null || sources.isEmpty()) {
|
||||
return sources;
|
||||
}
|
||||
|
||||
if (node instanceof Block block) {
|
||||
List<ControlFlowGraph.CfgNode> currentSources = sources;
|
||||
for (Object stmtObj : block.statements()) {
|
||||
currentSources = buildFlow((ASTNode) stmtObj, currentSources, cfg);
|
||||
}
|
||||
return currentSources;
|
||||
}
|
||||
|
||||
if (node instanceof IfStatement ifStmt) {
|
||||
// Condition node
|
||||
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(ifStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
cfg.addNode(condNode);
|
||||
for (ControlFlowGraph.CfgNode src : sources) {
|
||||
src.addSuccessor(condNode);
|
||||
}
|
||||
|
||||
List<ControlFlowGraph.CfgNode> thenExits = buildFlow(ifStmt.getThenStatement(), List.of(condNode), cfg);
|
||||
List<ControlFlowGraph.CfgNode> elseExits;
|
||||
if (ifStmt.getElseStatement() != null) {
|
||||
elseExits = buildFlow(ifStmt.getElseStatement(), List.of(condNode), cfg);
|
||||
} else {
|
||||
elseExits = List.of(condNode);
|
||||
}
|
||||
|
||||
List<ControlFlowGraph.CfgNode> combinedExits = new ArrayList<>();
|
||||
combinedExits.addAll(thenExits);
|
||||
combinedExits.addAll(elseExits);
|
||||
return combinedExits;
|
||||
}
|
||||
|
||||
if (node instanceof WhileStatement whileStmt) {
|
||||
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(whileStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
cfg.addNode(condNode);
|
||||
for (ControlFlowGraph.CfgNode src : sources) {
|
||||
src.addSuccessor(condNode);
|
||||
}
|
||||
|
||||
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(whileStmt.getBody(), List.of(condNode), cfg);
|
||||
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||
exit.addSuccessor(condNode); // Loop back
|
||||
}
|
||||
|
||||
return List.of(condNode); // Exit loop when cond is false
|
||||
}
|
||||
|
||||
if (node instanceof DoStatement doStmt) {
|
||||
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
cfg.addNode(condNode);
|
||||
|
||||
List<ControlFlowGraph.CfgNode> bodySources = new ArrayList<>(sources);
|
||||
bodySources.add(condNode);
|
||||
|
||||
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(doStmt.getBody(), bodySources, cfg);
|
||||
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||
exit.addSuccessor(condNode);
|
||||
}
|
||||
return List.of(condNode);
|
||||
}
|
||||
|
||||
if (node instanceof ForStatement forStmt) {
|
||||
// Initializers
|
||||
List<ControlFlowGraph.CfgNode> currentSources = sources;
|
||||
for (Object initObj : forStmt.initializers()) {
|
||||
ControlFlowGraph.CfgNode initNode = new ControlFlowGraph.CfgNode((ASTNode) initObj, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
cfg.addNode(initNode);
|
||||
for (ControlFlowGraph.CfgNode src : currentSources) {
|
||||
src.addSuccessor(initNode);
|
||||
}
|
||||
currentSources = List.of(initNode);
|
||||
}
|
||||
|
||||
// Condition
|
||||
ControlFlowGraph.CfgNode condNode;
|
||||
if (forStmt.getExpression() != null) {
|
||||
condNode = new ControlFlowGraph.CfgNode(forStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
} else {
|
||||
condNode = new ControlFlowGraph.CfgNode(forStmt, ControlFlowGraph.CfgNodeType.STATEMENT); // Dummy node
|
||||
}
|
||||
cfg.addNode(condNode);
|
||||
for (ControlFlowGraph.CfgNode src : currentSources) {
|
||||
src.addSuccessor(condNode);
|
||||
}
|
||||
|
||||
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(forStmt.getBody(), List.of(condNode), cfg);
|
||||
|
||||
// Updaters
|
||||
List<ControlFlowGraph.CfgNode> updaterSources = bodyExits;
|
||||
for (Object updateObj : forStmt.updaters()) {
|
||||
ControlFlowGraph.CfgNode updateNode = new ControlFlowGraph.CfgNode((ASTNode) updateObj, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
cfg.addNode(updateNode);
|
||||
for (ControlFlowGraph.CfgNode src : updaterSources) {
|
||||
src.addSuccessor(updateNode);
|
||||
}
|
||||
updaterSources = List.of(updateNode);
|
||||
}
|
||||
|
||||
for (ControlFlowGraph.CfgNode src : updaterSources) {
|
||||
src.addSuccessor(condNode); // Loop back
|
||||
}
|
||||
|
||||
return List.of(condNode);
|
||||
}
|
||||
|
||||
if (node instanceof EnhancedForStatement effStmt) {
|
||||
ControlFlowGraph.CfgNode loopNode = new ControlFlowGraph.CfgNode(effStmt.getParameter(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
cfg.addNode(loopNode);
|
||||
for (ControlFlowGraph.CfgNode src : sources) {
|
||||
src.addSuccessor(loopNode);
|
||||
}
|
||||
|
||||
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(effStmt.getBody(), List.of(loopNode), cfg);
|
||||
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||
exit.addSuccessor(loopNode); // Loop back
|
||||
}
|
||||
|
||||
return List.of(loopNode);
|
||||
}
|
||||
|
||||
if (node instanceof ReturnStatement returnStmt) {
|
||||
ControlFlowGraph.CfgNode retNode = new ControlFlowGraph.CfgNode(returnStmt, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
cfg.addNode(retNode);
|
||||
for (ControlFlowGraph.CfgNode src : sources) {
|
||||
src.addSuccessor(retNode);
|
||||
}
|
||||
// Return flows to exit, not to subsequent sequential nodes
|
||||
retNode.addSuccessor(cfg.getExitNode());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Default: general statement
|
||||
ControlFlowGraph.CfgNode stmtNode = new ControlFlowGraph.CfgNode(node, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||
cfg.addNode(stmtNode);
|
||||
for (ControlFlowGraph.CfgNode src : sources) {
|
||||
src.addSuccessor(stmtNode);
|
||||
}
|
||||
return List.of(stmtNode);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,9 +251,37 @@ public class CodebaseContext {
|
||||
}
|
||||
}
|
||||
|
||||
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 type : rd.bodyDeclarations()) {
|
||||
if (type instanceof TypeDeclaration nestedTd) {
|
||||
indexType(nestedTd, fqn, javaFile);
|
||||
} else if (type instanceof org.eclipse.jdt.core.dom.RecordDeclaration nestedRd) {
|
||||
indexRecord(nestedRd, fqn, javaFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getImplementations(String interfaceName) {
|
||||
Set<String> allImpls = new HashSet<>();
|
||||
collectImplementations(interfaceName, allImpls, new HashSet<>());
|
||||
System.out.println("GET IMPLEMENTATIONS FOR: " + interfaceName + " -> " + allImpls);
|
||||
return new ArrayList<>(allImpls);
|
||||
}
|
||||
|
||||
@@ -289,6 +333,10 @@ public class CodebaseContext {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getEnumValuesMap() {
|
||||
return enumValues;
|
||||
}
|
||||
|
||||
public List<String> getEnumValues(String fqnOrSimpleName) {
|
||||
if (fqnOrSimpleName == null) return null;
|
||||
List<String> values = enumValues.get(fqnOrSimpleName);
|
||||
@@ -300,110 +348,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 +356,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 +402,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,6 +446,14 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
public String getSuperclassFqn(TypeDeclaration td) {
|
||||
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;
|
||||
|
||||
@@ -513,6 +475,54 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||
ITypeBinding binding = td.resolveBinding();
|
||||
if (binding != null && includeSuper) {
|
||||
IMethodBinding methodBinding = findMethodBinding(binding, methodName);
|
||||
if (methodBinding != 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) {
|
||||
for (MethodDeclaration method : td.getMethods()) {
|
||||
if (method.getName().getIdentifier().equals(methodName)) {
|
||||
return method;
|
||||
@@ -524,7 +534,7 @@ public class CodebaseContext {
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
||||
if (superTd != null) {
|
||||
return findMethodDeclaration(superTd, methodName, true);
|
||||
return findMethodDeclarationLegacy(superTd, methodName, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +544,7 @@ public class CodebaseContext {
|
||||
String interfaceName = extractTypeName(interfaceType);
|
||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (CompilationUnit) td.getRoot());
|
||||
if (interfaceTd != null) {
|
||||
MethodDeclaration found = findMethodDeclaration(interfaceTd, methodName, true);
|
||||
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true);
|
||||
if (found != null) return found;
|
||||
}
|
||||
}
|
||||
@@ -542,15 +552,103 @@ 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) {
|
||||
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 = (CompilationUnit) atd.getRoot();
|
||||
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
|
||||
String fqn = sb.toString();
|
||||
return packageName.isEmpty() ? fqn : packageName + "." + fqn;
|
||||
@@ -624,10 +722,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 +751,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 +762,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.*;
|
||||
|
||||
public class ControlFlowGraph {
|
||||
private final MethodDeclaration method;
|
||||
private final CfgNode entryNode = new CfgNode(null, CfgNodeType.ENTRY);
|
||||
private final CfgNode exitNode = new CfgNode(null, CfgNodeType.EXIT);
|
||||
private final List<CfgNode> nodes = new ArrayList<>();
|
||||
|
||||
public enum CfgNodeType {
|
||||
ENTRY,
|
||||
EXIT,
|
||||
STATEMENT
|
||||
}
|
||||
|
||||
public static class CfgNode {
|
||||
private final ASTNode astNode;
|
||||
private final CfgNodeType type;
|
||||
private final Set<CfgNode> predecessors = new HashSet<>();
|
||||
private final Set<CfgNode> successors = new HashSet<>();
|
||||
|
||||
public CfgNode(ASTNode astNode, CfgNodeType type) {
|
||||
this.astNode = astNode;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ASTNode getAstNode() {
|
||||
return astNode;
|
||||
}
|
||||
|
||||
public CfgNodeType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Set<CfgNode> getPredecessors() {
|
||||
return predecessors;
|
||||
}
|
||||
|
||||
public Set<CfgNode> getSuccessors() {
|
||||
return successors;
|
||||
}
|
||||
|
||||
public void addSuccessor(CfgNode succ) {
|
||||
if (succ != null) {
|
||||
this.successors.add(succ);
|
||||
succ.predecessors.add(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ControlFlowGraph(MethodDeclaration method) {
|
||||
this.method = method;
|
||||
this.nodes.add(entryNode);
|
||||
this.nodes.add(exitNode);
|
||||
}
|
||||
|
||||
public MethodDeclaration getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public CfgNode getEntryNode() {
|
||||
return entryNode;
|
||||
}
|
||||
|
||||
public CfgNode getExitNode() {
|
||||
return exitNode;
|
||||
}
|
||||
|
||||
public List<CfgNode> getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public void addNode(CfgNode node) {
|
||||
this.nodes.add(node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import java.util.List;
|
||||
|
||||
public interface DataFlowModel {
|
||||
/**
|
||||
* Resolves all possible Expressions that define the value of the given expression
|
||||
* at its location in the control flow.
|
||||
*/
|
||||
List<Expression> getReachingDefinitions(Expression expr);
|
||||
|
||||
/**
|
||||
* Resolves the single constant/literal value if all definitions converge,
|
||||
* or if a single value is expected.
|
||||
*/
|
||||
String resolveValue(Expression expr, CodebaseContext context);
|
||||
}
|
||||
@@ -0,0 +1,871 @@
|
||||
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) {
|
||||
objectFieldBindings.computeIfAbsent(cic, k -> buildFieldBindingsFromCic(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> 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 =
|
||||
objectFieldBindings.computeIfAbsent(cic, k -> buildFieldBindingsFromCic((ClassInstanceCreation) k));
|
||||
|
||||
// Apply flow-sensitive intermediate local variable mutations
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(sn);
|
||||
if (enclosingMethod != null) {
|
||||
ReachingDefinitions rd = getReachingDefinitionsForMethod(enclosingMethod);
|
||||
if (rd != null) {
|
||||
Set<ASTNode> defs = rd.getReachingDefinitions(sn, sn.getIdentifier());
|
||||
for (ASTNode def : defs) {
|
||||
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
|
||||
if (defExpr == cic || isExpressionWrapping(defExpr, cic)) {
|
||||
applyIntermediateMutations(sn, def, mi, enclosingMethod, concreteFieldBindings, paramBindings, visited, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targets.add(new TargetMethod(md, concreteFieldBindings));
|
||||
}
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Polymorphic target discovery (including interface implementations)
|
||||
ITypeBinding declaringClass = mb.getDeclaringClass();
|
||||
if (declaringClass != null) {
|
||||
String declaringClassFqn = declaringClass.getErasure().getQualifiedName();
|
||||
if (declaringClassFqn != null && !declaringClassFqn.isEmpty()) {
|
||||
// Find all known implementation subclasses in the codebase
|
||||
List<String> implClassFqns = context.getImplementations(declaringClassFqn);
|
||||
if (implClassFqns != null && !implClassFqns.isEmpty()) {
|
||||
for (String implFqn : implClassFqns) {
|
||||
TypeDeclaration td = context.getTypeDeclaration(implFqn);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = findMethodDeclarationInType(td.resolveBinding(), mb);
|
||||
if (md != null) {
|
||||
// For polymorphic targets with unknown instance, we don't have concrete field bindings
|
||||
Map<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
|
||||
targets.add(new TargetMethod(md, targetFieldBindings));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the declaring class is a class (not an interface), or if the declared method itself is concrete/has a body,
|
||||
// we should also include the declared method itself in the polymorphic targets.
|
||||
MethodDeclaration declaredMd = findMethodDeclaration(mb);
|
||||
if (declaredMd != null && declaredMd.getBody() != null) {
|
||||
boolean alreadyAdded = false;
|
||||
for (TargetMethod tm : targets) {
|
||||
if (tm.declaration == declaredMd) {
|
||||
alreadyAdded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadyAdded) {
|
||||
Map<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
|
||||
targets.add(new TargetMethod(declaredMd, targetFieldBindings));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no polymorphic subclass targets were found, fallback to the declared method itself
|
||||
if (targets.isEmpty()) {
|
||||
MethodDeclaration md = findMethodDeclaration(mb);
|
||||
if (md != null) {
|
||||
Map<IVariableBinding, Expression> targetFieldBindings = isThisReceiver ? new HashMap<>(instanceFieldBindings) : new HashMap<>();
|
||||
targets.add(new TargetMethod(md, targetFieldBindings));
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
private boolean isExpressionWrapping(Expression outer, Expression inner) {
|
||||
Expression current = outer;
|
||||
while (current != null) {
|
||||
if (current == inner) return true;
|
||||
if (current instanceof ParenthesizedExpression pe) {
|
||||
current = pe.getExpression();
|
||||
} else if (current instanceof CastExpression ce) {
|
||||
current = ce.getExpression();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ControlFlowGraph.CfgNode findCfgNode(ControlFlowGraph cfg, ASTNode astNode) {
|
||||
ASTNode current = astNode;
|
||||
while (current != null) {
|
||||
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||
if (node.getAstNode() == current) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isTargetVariable(Expression expr, IVariableBinding targetVar, String targetName) {
|
||||
if (expr instanceof SimpleName sn) {
|
||||
IBinding b = sn.resolveBinding();
|
||||
if (b instanceof IVariableBinding vb) {
|
||||
if (targetVar != null && vb.getKey().equals(targetVar.getKey())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (targetName != null && sn.getIdentifier().equals(targetName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void evaluateMethodMutations(
|
||||
MethodDeclaration md,
|
||||
List<?> arguments,
|
||||
Map<IVariableBinding, Expression> fieldBindings) {
|
||||
if (md == null || md.getBody() == null) return;
|
||||
|
||||
Map<IVariableBinding, Expression> currentParamValues = new HashMap<>();
|
||||
List<?> parameters = md.parameters();
|
||||
int count = Math.min(parameters.size(), arguments.size());
|
||||
for (int i = 0; i < count; i++) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) parameters.get(i);
|
||||
IVariableBinding paramVb = svd.resolveBinding();
|
||||
if (paramVb != null) {
|
||||
Expression arg = (Expression) arguments.get(i);
|
||||
currentParamValues.put(paramVb, arg);
|
||||
}
|
||||
}
|
||||
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(Assignment assignment) {
|
||||
Expression lhs = assignment.getLeftHandSide();
|
||||
Expression rhs = assignment.getRightHandSide();
|
||||
IVariableBinding fieldVb = null;
|
||||
if (lhs instanceof FieldAccess fa) {
|
||||
if (fa.getExpression() instanceof ThisExpression) {
|
||||
IBinding b = fa.getName().resolveBinding();
|
||||
if (b instanceof IVariableBinding vb && vb.isField()) {
|
||||
fieldVb = vb;
|
||||
}
|
||||
}
|
||||
} else if (lhs instanceof SimpleName sn) {
|
||||
IBinding b = sn.resolveBinding();
|
||||
if (b instanceof IVariableBinding vb && vb.isField()) {
|
||||
fieldVb = vb;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldVb != null) {
|
||||
Expression resolvedRhs = resolveParamValue(rhs, currentParamValues);
|
||||
fieldBindings.put(fieldVb, resolvedRhs);
|
||||
}
|
||||
return super.visit(assignment);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void findAndApplyMutations(
|
||||
ASTNode node,
|
||||
IVariableBinding targetVar,
|
||||
String targetName,
|
||||
Map<IVariableBinding, Expression> fieldBindings,
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Set<ASTNode> visited,
|
||||
int depth) {
|
||||
node.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(Assignment assignment) {
|
||||
Expression lhs = assignment.getLeftHandSide();
|
||||
Expression rhs = assignment.getRightHandSide();
|
||||
IVariableBinding fieldVb = null;
|
||||
if (lhs instanceof FieldAccess fa) {
|
||||
if (isTargetVariable(fa.getExpression(), targetVar, targetName)) {
|
||||
IBinding b = fa.getName().resolveBinding();
|
||||
if (b instanceof IVariableBinding vb && vb.isField()) {
|
||||
fieldVb = vb;
|
||||
}
|
||||
}
|
||||
} else if (lhs instanceof QualifiedName qn) {
|
||||
if (isTargetVariable(qn.getQualifier(), targetVar, targetName)) {
|
||||
IBinding b = qn.getName().resolveBinding();
|
||||
if (b instanceof IVariableBinding vb && vb.isField()) {
|
||||
fieldVb = vb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldVb != null) {
|
||||
List<Expression> resolvedRhs = getReachingDefinitions(rhs, visited, paramBindings, fieldBindings, depth + 1);
|
||||
if (!resolvedRhs.isEmpty()) {
|
||||
fieldBindings.put(fieldVb, resolvedRhs.get(0));
|
||||
} else {
|
||||
fieldBindings.put(fieldVb, rhs);
|
||||
}
|
||||
}
|
||||
return super.visit(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodInvocation mi) {
|
||||
Expression receiver = mi.getExpression();
|
||||
if (receiver != null && isTargetVariable(receiver, targetVar, targetName)) {
|
||||
IMethodBinding mb = mi.resolveMethodBinding();
|
||||
if (mb != null) {
|
||||
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, fieldBindings, depth + 1);
|
||||
for (TargetMethod target : targets) {
|
||||
if (target.declaration != null) {
|
||||
evaluateMethodMutations(target.declaration, mi.arguments(), fieldBindings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(mi);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void applyIntermediateMutations(
|
||||
SimpleName receiverSimpleName,
|
||||
ASTNode defNode,
|
||||
ASTNode useNode,
|
||||
MethodDeclaration enclosingMethod,
|
||||
Map<IVariableBinding, Expression> fieldBindings,
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Set<ASTNode> visited,
|
||||
int depth) {
|
||||
|
||||
IBinding b = receiverSimpleName.resolveBinding();
|
||||
IVariableBinding targetVar = (b instanceof IVariableBinding vb) ? vb : null;
|
||||
String targetName = receiverSimpleName.getIdentifier();
|
||||
|
||||
ControlFlowGraph cfg = cfgCache.computeIfAbsent(enclosingMethod, CfgBuilder::build);
|
||||
if (cfg == null) return;
|
||||
|
||||
ControlFlowGraph.CfgNode defCfgNode = findCfgNode(cfg, defNode);
|
||||
ControlFlowGraph.CfgNode useCfgNode = findCfgNode(cfg, useNode);
|
||||
|
||||
if (defCfgNode != null && useCfgNode != null) {
|
||||
Set<ControlFlowGraph.CfgNode> reachFromD = new HashSet<>();
|
||||
Queue<ControlFlowGraph.CfgNode> queue = new LinkedList<>();
|
||||
queue.add(defCfgNode);
|
||||
reachFromD.add(defCfgNode);
|
||||
while (!queue.isEmpty()) {
|
||||
ControlFlowGraph.CfgNode curr = queue.poll();
|
||||
for (ControlFlowGraph.CfgNode succ : curr.getSuccessors()) {
|
||||
if (reachFromD.add(succ)) {
|
||||
queue.add(succ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<ControlFlowGraph.CfgNode> reachToU = new HashSet<>();
|
||||
queue.clear();
|
||||
queue.add(useCfgNode);
|
||||
reachToU.add(useCfgNode);
|
||||
while (!queue.isEmpty()) {
|
||||
ControlFlowGraph.CfgNode curr = queue.poll();
|
||||
for (ControlFlowGraph.CfgNode pred : curr.getPredecessors()) {
|
||||
if (reachToU.add(pred)) {
|
||||
queue.add(pred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<ControlFlowGraph.CfgNode> pathNodes = new HashSet<>(reachFromD);
|
||||
pathNodes.retainAll(reachToU);
|
||||
|
||||
List<ControlFlowGraph.CfgNode> orderedPathNodes = new ArrayList<>(pathNodes);
|
||||
List<ControlFlowGraph.CfgNode> allNodes = cfg.getNodes();
|
||||
orderedPathNodes.sort(Comparator.comparingInt(allNodes::indexOf));
|
||||
|
||||
for (ControlFlowGraph.CfgNode node : orderedPathNodes) {
|
||||
if (node != defCfgNode && node != useCfgNode && node.getAstNode() != null) {
|
||||
findAndApplyMutations(node.getAstNode(), targetVar, targetName, fieldBindings, paramBindings, visited, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MethodDeclaration findMethodDeclarationInType(ITypeBinding typeBinding, IMethodBinding mb) {
|
||||
if (typeBinding == null || mb == null) return null;
|
||||
|
||||
// 1. Try to find it in the class hierarchy (including typeBinding itself)
|
||||
ITypeBinding current = typeBinding;
|
||||
while (current != null) {
|
||||
MethodDeclaration md = findMethodInTypeDeclarationOnly(current, mb);
|
||||
if (md != null) return md;
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
|
||||
// 2. Try to find it in the interface hierarchy of typeBinding
|
||||
Set<String> visited = new HashSet<>();
|
||||
Queue<ITypeBinding> queue = new LinkedList<>();
|
||||
queue.add(typeBinding);
|
||||
while (!queue.isEmpty()) {
|
||||
ITypeBinding tb = queue.poll();
|
||||
if (tb == null) continue;
|
||||
String key = tb.getKey();
|
||||
if (key != null && !visited.add(key)) continue;
|
||||
|
||||
MethodDeclaration md = findMethodInTypeDeclarationOnly(tb, mb);
|
||||
if (md != null) return md;
|
||||
|
||||
for (ITypeBinding interf : tb.getInterfaces()) {
|
||||
queue.add(interf);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private MethodDeclaration findMethodInTypeDeclarationOnly(ITypeBinding typeBinding, IMethodBinding mb) {
|
||||
String classFqn = typeBinding.getErasure().getQualifiedName();
|
||||
if (classFqn != null && !classFqn.isEmpty()) {
|
||||
TypeDeclaration td = context.getTypeDeclaration(classFqn);
|
||||
if (td != null) {
|
||||
for (MethodDeclaration md : td.getMethods()) {
|
||||
IMethodBinding candidateMb = md.resolveBinding();
|
||||
if (candidateMb != null && candidateMb.getKey().equals(mb.getKey())) {
|
||||
return md;
|
||||
}
|
||||
}
|
||||
for (MethodDeclaration md : td.getMethods()) {
|
||||
if (md.getName().getIdentifier().equals(mb.getName()) &&
|
||||
md.parameters().size() == mb.getParameterTypes().length) {
|
||||
return md;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MethodDeclaration findMethodDeclaration(IMethodBinding mb) {
|
||||
if (mb == null) return null;
|
||||
return findMethodDeclarationInType(mb.getDeclaringClass(), mb);
|
||||
}
|
||||
|
||||
private List<ReturnStatement> findReturnStatements(ASTNode node) {
|
||||
List<ReturnStatement> returns = new ArrayList<>();
|
||||
node.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement rs) {
|
||||
returns.add(rs);
|
||||
return super.visit(rs);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(LambdaExpression le) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean visit(AnonymousClassDeclaration acd) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean visit(TypeDeclarationStatement tds) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return returns;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||
if (expr == null) return null;
|
||||
|
||||
List<Expression> defs = getReachingDefinitions(expr);
|
||||
if (defs.isEmpty()) {
|
||||
return context.resolveExpression(expr);
|
||||
}
|
||||
|
||||
String firstVal = null;
|
||||
for (Expression def : defs) {
|
||||
String val = context.resolveExpression(def);
|
||||
if (val != null) {
|
||||
if (firstVal == null) {
|
||||
firstVal = val;
|
||||
} else if (!firstVal.equals(val)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstVal != null ? firstVal : context.resolveExpression(expr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||
import java.util.List;
|
||||
|
||||
public class LegacyDataFlowModel implements DataFlowModel {
|
||||
private final VariableTracer variableTracer;
|
||||
|
||||
public LegacyDataFlowModel(VariableTracer variableTracer) {
|
||||
this.variableTracer = variableTracer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Expression> getReachingDefinitions(Expression expr) {
|
||||
if (variableTracer == null || expr == null) {
|
||||
return expr != null ? List.of(expr) : List.of();
|
||||
}
|
||||
return variableTracer.traceVariableAll(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||
if (expr == null) return null;
|
||||
if (variableTracer == null) {
|
||||
return context.resolveExpression(expr);
|
||||
}
|
||||
Expression traced = variableTracer.traceVariable(expr);
|
||||
return context.resolveExpression(traced);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.*;
|
||||
|
||||
public class ReachingDefinitions {
|
||||
private final ControlFlowGraph cfg;
|
||||
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> inMap = new HashMap<>();
|
||||
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> outMap = new HashMap<>();
|
||||
|
||||
public ReachingDefinitions(ControlFlowGraph cfg) {
|
||||
this.cfg = cfg;
|
||||
analyze();
|
||||
}
|
||||
|
||||
private void analyze() {
|
||||
List<ControlFlowGraph.CfgNode> cfgNodes = cfg.getNodes();
|
||||
Map<ControlFlowGraph.CfgNode, ASTNode> nodeDefs = new HashMap<>();
|
||||
Map<String, Set<ASTNode>> varToDefs = new HashMap<>();
|
||||
|
||||
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||
ASTNode ast = node.getAstNode();
|
||||
if (ast == null) continue;
|
||||
|
||||
ASTNode def = findDefinition(ast);
|
||||
if (def != null) {
|
||||
nodeDefs.put(node, def);
|
||||
String varName = getDefinedVariableName(def);
|
||||
if (varName != null) {
|
||||
varToDefs.computeIfAbsent(varName, k -> new HashSet<>()).add(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||
inMap.put(node, new HashSet<>());
|
||||
outMap.put(node, new HashSet<>());
|
||||
}
|
||||
|
||||
boolean changed = true;
|
||||
int iterations = 0;
|
||||
int maxIterations = 5000;
|
||||
while (changed && iterations < maxIterations) {
|
||||
iterations++;
|
||||
changed = false;
|
||||
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||
Set<ASTNode> newIn = new HashSet<>();
|
||||
for (ControlFlowGraph.CfgNode pred : node.getPredecessors()) {
|
||||
newIn.addAll(outMap.get(pred));
|
||||
}
|
||||
|
||||
inMap.put(node, newIn);
|
||||
|
||||
Set<ASTNode> newOut = new HashSet<>(newIn);
|
||||
ASTNode def = nodeDefs.get(node);
|
||||
if (def != null) {
|
||||
String varName = getDefinedVariableName(def);
|
||||
if (varName != null) {
|
||||
Set<ASTNode> allDefs = varToDefs.get(varName);
|
||||
if (allDefs != null) {
|
||||
newOut.removeAll(allDefs);
|
||||
}
|
||||
}
|
||||
newOut.add(def);
|
||||
}
|
||||
|
||||
Set<ASTNode> oldOut = outMap.put(node, newOut);
|
||||
if (!newOut.equals(oldOut)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (iterations >= maxIterations) {
|
||||
// Log warning or print to stderr
|
||||
System.err.println("Warning: ReachingDefinitions analysis reached max iterations (" + maxIterations + ") and terminated early.");
|
||||
}
|
||||
}
|
||||
|
||||
public Set<ASTNode> getReachingDefinitions(ASTNode usageNode, String varName) {
|
||||
ControlFlowGraph.CfgNode cfgNode = findCfgNodeForUsage(usageNode);
|
||||
if (cfgNode == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<ASTNode> reaching = new HashSet<>();
|
||||
Set<ASTNode> inDefs = inMap.get(cfgNode);
|
||||
if (inDefs != null) {
|
||||
for (ASTNode def : inDefs) {
|
||||
if (varName.equals(getDefinedVariableName(def))) {
|
||||
reaching.add(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reaching;
|
||||
}
|
||||
|
||||
private ControlFlowGraph.CfgNode findCfgNodeForUsage(ASTNode usageNode) {
|
||||
ASTNode current = usageNode;
|
||||
while (current != null) {
|
||||
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||
if (node.getAstNode() == current) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ASTNode findDefinition(ASTNode ast) {
|
||||
if (ast instanceof VariableDeclarationStatement vds) {
|
||||
if (!vds.fragments().isEmpty()) {
|
||||
return (ASTNode) vds.fragments().get(0);
|
||||
}
|
||||
}
|
||||
if (ast instanceof VariableDeclarationFragment) {
|
||||
return ast;
|
||||
}
|
||||
if (ast instanceof SingleVariableDeclaration) {
|
||||
return ast;
|
||||
}
|
||||
if (ast instanceof Assignment) {
|
||||
return ast;
|
||||
}
|
||||
if (ast instanceof ExpressionStatement es) {
|
||||
return findDefinition(es.getExpression());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getDefinedVariableName(ASTNode def) {
|
||||
if (def instanceof VariableDeclarationFragment frag) {
|
||||
return frag.getName().getIdentifier();
|
||||
}
|
||||
if (def instanceof SingleVariableDeclaration svd) {
|
||||
return svd.getName().getIdentifier();
|
||||
}
|
||||
if (def instanceof Assignment assign) {
|
||||
if (assign.getLeftHandSide() instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Expression getDefinitionExpression(ASTNode def) {
|
||||
if (def instanceof VariableDeclarationFragment frag) {
|
||||
return frag.getInitializer();
|
||||
}
|
||||
if (def instanceof Assignment assign) {
|
||||
return assign.getRightHandSide();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
public class StateResolver {
|
||||
private final CodebaseContext context;
|
||||
|
||||
public StateResolver(CodebaseContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public State resolveState(Expression expr, CompilationUnit cu) {
|
||||
if (expr == null)
|
||||
return null;
|
||||
|
||||
// 1. Check for constants
|
||||
String resolvedValue = context.getConstantResolver().resolve(expr, context);
|
||||
if (resolvedValue != null) {
|
||||
return State.of(expr.toString(), resolvedValue);
|
||||
}
|
||||
|
||||
// 2. Check for @Value fields (SimpleName)
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String fieldName = sn.getIdentifier();
|
||||
TypeDeclaration td = findEnclosingType(sn);
|
||||
if (td != null) {
|
||||
String value = findValueFromField(td, fieldName);
|
||||
if (value != null) {
|
||||
return State.of(fieldName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expr instanceof StringLiteral sl)
|
||||
return State.of(sl.getLiteralValue());
|
||||
|
||||
String raw = expr.toString();
|
||||
if (expr instanceof QualifiedName qn) {
|
||||
String full = resolveQualifiedName(qn, cu);
|
||||
return State.of(raw, full);
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String full = resolveSimpleName(sn, cu);
|
||||
return State.of(raw, full);
|
||||
}
|
||||
return State.of(raw);
|
||||
}
|
||||
|
||||
private String findValueFromField(TypeDeclaration td, String fieldName) {
|
||||
for (FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
if (fragObj instanceof VariableDeclarationFragment fragment) {
|
||||
if (fragment.getName().getIdentifier().equals(fieldName)) {
|
||||
// Check for @Value annotation
|
||||
for (Object mod : fd.modifiers()) {
|
||||
if (mod instanceof Annotation ann) {
|
||||
String name = ann.getTypeName().getFullyQualifiedName();
|
||||
if (name.endsWith("Value")) {
|
||||
return extractAnnotationValue(ann);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractAnnotationValue(Annotation ann) {
|
||||
if (ann instanceof SingleMemberAnnotation sma) {
|
||||
return stripQuotes(sma.getValue().toString());
|
||||
} else if (ann instanceof NormalAnnotation na) {
|
||||
for (Object pairObj : na.values()) {
|
||||
MemberValuePair pair = (MemberValuePair) pairObj;
|
||||
if ("value".equals(pair.getName().getIdentifier())) {
|
||||
return stripQuotes(pair.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String stripQuotes(String s) {
|
||||
if (s == null) return null;
|
||||
return s.replaceAll("^\"|\"$", "");
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
|
||||
private String resolveQualifiedName(QualifiedName qn, CompilationUnit cu) {
|
||||
String qualifier = qn.getQualifier().toString();
|
||||
String name = qn.getName().getIdentifier();
|
||||
TypeDeclaration td = context.getTypeDeclaration(qualifier, cu);
|
||||
if (td != null)
|
||||
return context.getFqn(td) + "." + name;
|
||||
return qn.getFullyQualifiedName();
|
||||
}
|
||||
|
||||
private String resolveSimpleName(SimpleName sn, CompilationUnit cu) {
|
||||
String name = sn.getIdentifier();
|
||||
if (cu == null)
|
||||
return name;
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + name))
|
||||
return impName;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -45,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()
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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" +
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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" : [ {
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 15,
|
||||
"polymorphicEvents" : null
|
||||
"polymorphicEvents" : [ "INHERITED_SUBMIT" ]
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -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" ]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" : [ {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[
|
||||
{
|
||||
"methodFqn": "ExternalTaxService.calculateTax",
|
||||
"event": "FINALIZE"
|
||||
}
|
||||
]
|
||||
@@ -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/
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package click.kamil.examples.statemachine.extended.config;
|
||||
|
||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
public abstract class AbstractBaseConfig {
|
||||
|
||||
@Bean
|
||||
public String inheritedBeanTester(@Qualifier("customName") QuirkService someService) {
|
||||
someService.doQuirk();
|
||||
return "inherited";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package click.kamil.examples.statemachine.extended.config;
|
||||
|
||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class BeanParameterTestConfig extends AbstractBaseConfig {
|
||||
|
||||
@Bean
|
||||
public String myBeanParamTester(@Qualifier("customName") QuirkService someService) {
|
||||
// We will use this method as an entry point in the test
|
||||
someService.doQuirk();
|
||||
return "tested";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package click.kamil.examples.statemachine.extended.config;
|
||||
|
||||
import click.kamil.examples.statemachine.extended.service.QuirkService;
|
||||
import click.kamil.examples.statemachine.extended.service.QualifierQuirkService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ConcreteReturnTypeConfig {
|
||||
|
||||
@Bean
|
||||
public QuirkService hiddenConcreteService() {
|
||||
return new QualifierQuirkService(); // It returns an interface, but the concrete type is QualifierQuirkService
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package click.kamil.examples.statemachine.extended.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class MockBeanConfig {
|
||||
|
||||
@Bean(name = {"customMockBean", "aliasMockBean"})
|
||||
public String myStringBean() {
|
||||
return "Hello World";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package click.kamil.examples.statemachine.extended.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.statemachine.config.EnableStateMachine;
|
||||
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine(name = "paymentStateMachine")
|
||||
public class PaymentStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("NEW")
|
||||
.state("AUTHORIZED")
|
||||
.state("CAPTURED")
|
||||
.state("DECLINED")
|
||||
.state("QUIRK1")
|
||||
.state("QUIRK2")
|
||||
.state("QUIRK3")
|
||||
.state("QUIRK4");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source("NEW").target("AUTHORIZED")
|
||||
.event("AUTHORIZE")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("AUTHORIZED").target("CAPTURED")
|
||||
.event("CAPTURE")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("NEW").target("DECLINED")
|
||||
.event("DECLINE")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("NEW").target("QUIRK1")
|
||||
.event("PRIMARY_EVENT")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("NEW").target("QUIRK2")
|
||||
.event("NAMED_EVENT")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("NEW").target("QUIRK3")
|
||||
.event("QUALIFIER_EVENT")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("NEW").target("QUIRK4")
|
||||
.event("FALLBACK_EVENT");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package click.kamil.examples.statemachine.extended.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AmbiguousA implements AmbiguousService {
|
||||
@Override
|
||||
public void doAmbig() {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package click.kamil.examples.statemachine.extended.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AmbiguousB implements AmbiguousService {
|
||||
@Override
|
||||
public void doAmbig() {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package click.kamil.examples.statemachine.extended.service;
|
||||
|
||||
public interface AmbiguousService {
|
||||
void doAmbig();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package click.kamil.examples.statemachine.extended.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class FallbackQuirkService implements QuirkService {
|
||||
|
||||
@Autowired
|
||||
@Qualifier("paymentStateMachine")
|
||||
private StateMachine<String, String> stateMachine;
|
||||
|
||||
@Override
|
||||
public void doQuirk() {
|
||||
stateMachine.sendEvent("FALLBACK_EVENT");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package click.kamil.examples.statemachine.extended.service;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class HighPriorityOrderedService implements OrderedService {
|
||||
@Override
|
||||
public void doAction() {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package click.kamil.examples.statemachine.extended.service;
|
||||
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Order(10)
|
||||
public class LowPriorityOrderedService implements OrderedService {
|
||||
@Override
|
||||
public void doAction() {}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user