Compare commits
7 Commits
613dfffd53
...
411b7a0e52
| Author | SHA1 | Date | |
|---|---|---|---|
| 411b7a0e52 | |||
| 82a70b5374 | |||
| 5d3333d494 | |||
| 7a5fc60ace | |||
| 9ede60049b | |||
| cc35546d01 | |||
| c2eff6d7bb |
@@ -1,5 +1,6 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.MachineScopeFilter;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
@@ -26,10 +27,12 @@ public class CallChainEnricher implements AnalysisEnricher {
|
||||
List<TriggerPoint> canonicalTriggers = intelligence.findTriggerPoints().stream()
|
||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||
.collect(Collectors.toList());
|
||||
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
|
||||
canonicalTriggers, result.getName(), context);
|
||||
|
||||
List<CallChain> chains = intelligence.findCallChains(
|
||||
result.getMetadata().getEntryPoints(),
|
||||
canonicalTriggers
|
||||
scopedTriggers
|
||||
);
|
||||
chains = chains.stream()
|
||||
.map(chain -> chain.getTriggerPoint() == null
|
||||
|
||||
@@ -72,7 +72,12 @@ public final class MachineScopeFilter {
|
||||
String path = entryPoint.getMetadata() != null ? entryPoint.getMetadata().get("path") : null;
|
||||
if (path != null) {
|
||||
String pathDomain = inferDomainFromPath(path);
|
||||
if (pathDomain != null && machineDomain != null && !domainsMatch(pathDomain, machineDomain)) {
|
||||
if (pathDomain != null && machineDomain != null && isKnownDomainScopedMachine(machineDomain)
|
||||
&& domainsMatch(pathDomain, machineDomain)) {
|
||||
return true;
|
||||
}
|
||||
if (pathDomain != null && machineDomain != null && isKnownDomainScopedMachine(machineDomain)
|
||||
&& !domainsMatch(pathDomain, machineDomain)) {
|
||||
return false;
|
||||
}
|
||||
if (pathDomain == null && path.contains("{")) {
|
||||
@@ -86,6 +91,11 @@ public final class MachineScopeFilter {
|
||||
return ROUTING.isRoutedToCorrectMachine(probe, machineName, context);
|
||||
}
|
||||
|
||||
private static boolean isKnownDomainScopedMachine(String machineDomain) {
|
||||
String normalized = normalizeDomainKey(machineDomain);
|
||||
return "ORDER".equals(normalized) || "DOCUMENT".equals(normalized) || "USER".equals(normalized);
|
||||
}
|
||||
|
||||
private static String inferDomainFromPath(String path) {
|
||||
String lower = path.toLowerCase(Locale.ROOT);
|
||||
if (lower.contains("/orders") || lower.contains("order.")) {
|
||||
|
||||
@@ -28,7 +28,10 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
if (context != null && isSingleStateMachine(context)
|
||||
&& (isErasedOrOpaqueType(triggerEventFqn) || isErasedOrOpaqueType(machineEventFqn))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (triggerStateFqn != null && machineStateFqn != null) {
|
||||
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
|
||||
@@ -37,6 +40,10 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
||||
return false; // Mismatch!
|
||||
}
|
||||
if (context != null && isSingleStateMachine(context)
|
||||
&& (isErasedOrOpaqueType(triggerStateFqn) || isErasedOrOpaqueType(machineStateFqn))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,6 +326,15 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
return !type1.equals(type2);
|
||||
}
|
||||
|
||||
private boolean isErasedOrOpaqueType(String typeFqn) {
|
||||
if (typeFqn == null) {
|
||||
return false;
|
||||
}
|
||||
String erased = eraseGenerics(typeFqn);
|
||||
return "java.lang.Object".equals(erased) || "Object".equals(erased)
|
||||
|| "java.io.Serializable".equals(erased) || "Serializable".equals(erased);
|
||||
}
|
||||
|
||||
private boolean isSingleStateMachine(CodebaseContext context) {
|
||||
return countStateMachines(context) <= 1;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ public final class AccessorFieldResolver {
|
||||
|
||||
public static List<String> resolveFieldConstants(
|
||||
AccessorSummary accessor,
|
||||
String requestingTypeFqn,
|
||||
CodebaseContext context,
|
||||
ConstructorAnalyzer constructorAnalyzer,
|
||||
CompilationUnit contextCu,
|
||||
@@ -35,11 +36,14 @@ public final class AccessorFieldResolver {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
String traceTypeFqn = requestingTypeFqn != null && !requestingTypeFqn.isBlank()
|
||||
? requestingTypeFqn
|
||||
: accessor.declaringFqn();
|
||||
TypeDeclaration typeDeclaration = contextCu != null
|
||||
? context.getTypeDeclaration(accessor.declaringFqn(), contextCu)
|
||||
? context.getTypeDeclaration(traceTypeFqn, contextCu)
|
||||
: null;
|
||||
if (typeDeclaration == null) {
|
||||
typeDeclaration = context.getTypeDeclaration(accessor.declaringFqn());
|
||||
typeDeclaration = context.getTypeDeclaration(traceTypeFqn);
|
||||
}
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
@@ -53,7 +57,7 @@ public final class AccessorFieldResolver {
|
||||
|
||||
if (constants.isEmpty()) {
|
||||
collectFieldInitializerConstants(
|
||||
typeDeclaration, accessor.fieldName(), constantExtractor, constants);
|
||||
typeDeclaration, accessor.fieldName(), constantExtractor, constants, context);
|
||||
}
|
||||
return constants;
|
||||
}
|
||||
@@ -62,8 +66,9 @@ public final class AccessorFieldResolver {
|
||||
TypeDeclaration typeDeclaration,
|
||||
String fieldName,
|
||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> constantExtractor,
|
||||
List<String> constants) {
|
||||
if (constantExtractor == null) {
|
||||
List<String> constants,
|
||||
CodebaseContext context) {
|
||||
if (constantExtractor == null || typeDeclaration == null) {
|
||||
return;
|
||||
}
|
||||
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
||||
@@ -75,5 +80,14 @@ public final class AccessorFieldResolver {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (constants.isEmpty() && context != null) {
|
||||
String superFqn = context.getSuperclassFqn(typeDeclaration);
|
||||
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
|
||||
TypeDeclaration superType = context.getTypeDeclaration(superFqn);
|
||||
if (superType != null) {
|
||||
collectFieldInitializerConstants(superType, fieldName, constantExtractor, constants, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class FieldTypeIndex {
|
||||
|
||||
private static final FieldTypeIndex EMPTY = new FieldTypeIndex(Map.of(), 0);
|
||||
|
||||
private final Map<String, Map<String, String>> byOwnerAndField;
|
||||
private final int fieldCount;
|
||||
|
||||
public FieldTypeIndex(Map<String, Map<String, String>> byOwnerAndField, int fieldCount) {
|
||||
this.byOwnerAndField = Collections.unmodifiableMap(deepCopy(byOwnerAndField));
|
||||
this.fieldCount = fieldCount;
|
||||
}
|
||||
|
||||
public static FieldTypeIndex empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public Optional<String> lookup(String ownerFqn, String fieldName) {
|
||||
if (ownerFqn == null || fieldName == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Map<String, String> fields = byOwnerAndField.get(ownerFqn);
|
||||
if (fields == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(fields.get(fieldName));
|
||||
}
|
||||
|
||||
public int fieldCount() {
|
||||
return fieldCount;
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, String>> deepCopy(Map<String, Map<String, String>> source) {
|
||||
Map<String, Map<String, String>> copy = new HashMap<>();
|
||||
for (Map.Entry<String, Map<String, String>> entry : source.entrySet()) {
|
||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
||||
import org.eclipse.jdt.core.dom.Modifier;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
public final class FieldTypeIndexBuilder {
|
||||
|
||||
private CodebaseContext context;
|
||||
|
||||
public FieldTypeIndex build(CodebaseContext context) {
|
||||
this.context = context;
|
||||
if (context == null) {
|
||||
return FieldTypeIndex.empty();
|
||||
}
|
||||
|
||||
Map<String, Map<String, String>> direct = new HashMap<>();
|
||||
for (String ownerFqn : context.getIndexedTypeFqns()) {
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
||||
if (typeDeclaration != null) {
|
||||
indexDeclaredFields(ownerFqn, typeDeclaration, direct);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Map<String, String>> effective = new HashMap<>();
|
||||
int fieldCount = 0;
|
||||
Map<String, Map<String, String>> cache = new HashMap<>();
|
||||
for (String ownerFqn : direct.keySet()) {
|
||||
Map<String, String> merged = mergeEffective(ownerFqn, direct, cache, new HashSet<>());
|
||||
if (!merged.isEmpty()) {
|
||||
effective.put(ownerFqn, merged);
|
||||
fieldCount += merged.size();
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Built field-type index: {} fields across {} types", fieldCount, effective.size());
|
||||
this.context = null;
|
||||
return new FieldTypeIndex(effective, fieldCount);
|
||||
}
|
||||
|
||||
private void indexDeclaredFields(
|
||||
String ownerFqn,
|
||||
TypeDeclaration typeDeclaration,
|
||||
Map<String, Map<String, String>> direct) {
|
||||
Map<String, String> fields = direct.computeIfAbsent(ownerFqn, ignored -> new HashMap<>());
|
||||
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
||||
String fieldType = fieldDeclaration.getType().toString();
|
||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
||||
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
|
||||
fields.put(fragment.getName().getIdentifier(), fieldType);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (TypeDeclaration nested : typeDeclaration.getTypes()) {
|
||||
indexDeclaredFields(context.getFqn(nested), nested, direct);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> mergeEffective(
|
||||
String ownerFqn,
|
||||
Map<String, Map<String, String>> direct,
|
||||
Map<String, Map<String, String>> cache,
|
||||
Set<String> visiting) {
|
||||
if (cache.containsKey(ownerFqn)) {
|
||||
return cache.get(ownerFqn);
|
||||
}
|
||||
if (!visiting.add(ownerFqn)) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
Map<String, String> merged = new HashMap<>();
|
||||
TypeDeclaration ownerType = context.getTypeDeclaration(ownerFqn);
|
||||
if (ownerType != null) {
|
||||
String superFqn = context.getSuperclassFqn(ownerType);
|
||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
||||
merged.putAll(mergeEffective(superFqn, direct, cache, visiting));
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> own = direct.get(ownerFqn);
|
||||
if (own != null) {
|
||||
merged.putAll(own);
|
||||
}
|
||||
|
||||
visiting.remove(ownerFqn);
|
||||
cache.put(ownerFqn, Map.copyOf(merged));
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
||||
|
||||
/**
|
||||
* Scan-time result for {@code owner.getX()} getter-chain hops: the next receiver type
|
||||
* and optional {@code new ...()} field initializer discovered on the backing field.
|
||||
*/
|
||||
public record GetterChainEdge(
|
||||
String ownerFqn,
|
||||
String getterName,
|
||||
String nextTypeFqn,
|
||||
ClassInstanceCreation fieldInitializerCic) {
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class GetterChainIndex {
|
||||
|
||||
private static final GetterChainIndex EMPTY = new GetterChainIndex(Map.of(), 0);
|
||||
|
||||
private final Map<String, Map<String, GetterChainEdge>> byOwnerAndGetter;
|
||||
private final int edgeCount;
|
||||
|
||||
public GetterChainIndex(Map<String, Map<String, GetterChainEdge>> byOwnerAndGetter, int edgeCount) {
|
||||
this.byOwnerAndGetter = Collections.unmodifiableMap(deepCopy(byOwnerAndGetter));
|
||||
this.edgeCount = edgeCount;
|
||||
}
|
||||
|
||||
public static GetterChainIndex empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public Optional<GetterChainEdge> lookup(String ownerFqn, String getterName) {
|
||||
if (ownerFqn == null || getterName == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Map<String, GetterChainEdge> getters = byOwnerAndGetter.get(ownerFqn);
|
||||
if (getters == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(getters.get(getterName));
|
||||
}
|
||||
|
||||
public int edgeCount() {
|
||||
return edgeCount;
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, GetterChainEdge>> deepCopy(
|
||||
Map<String, Map<String, GetterChainEdge>> source) {
|
||||
Map<String, Map<String, GetterChainEdge>> copy = new HashMap<>();
|
||||
for (Map.Entry<String, Map<String, GetterChainEdge>> entry : source.entrySet()) {
|
||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
|
||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public final class GetterChainIndexBuilder {
|
||||
|
||||
public GetterChainIndex build(CodebaseContext context, AccessorIndex accessorIndex) {
|
||||
if (context == null || accessorIndex == null) {
|
||||
return GetterChainIndex.empty();
|
||||
}
|
||||
|
||||
FieldInitializerFinder fieldInitializerFinder = new FieldInitializerFinder(context);
|
||||
Map<String, Map<String, GetterChainEdge>> edges = new HashMap<>();
|
||||
int edgeCount = 0;
|
||||
|
||||
for (Map.Entry<String, Map<String, AccessorSummary>> ownerEntry : accessorIndex.asMap().entrySet()) {
|
||||
String ownerFqn = ownerEntry.getKey();
|
||||
for (AccessorSummary accessor : ownerEntry.getValue().values()) {
|
||||
if (!accessor.isGetter()) {
|
||||
continue;
|
||||
}
|
||||
GetterChainEdge edge = buildEdge(context, fieldInitializerFinder, ownerFqn, accessor);
|
||||
if (edge == null) {
|
||||
continue;
|
||||
}
|
||||
edges.computeIfAbsent(ownerFqn, ignored -> new HashMap<>())
|
||||
.put(accessor.methodName(), edge);
|
||||
edgeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Built getter-chain index: {} edges across {} types", edgeCount, edges.size());
|
||||
return new GetterChainIndex(edges, edgeCount);
|
||||
}
|
||||
|
||||
private static GetterChainEdge buildEdge(
|
||||
CodebaseContext context,
|
||||
FieldInitializerFinder fieldInitializerFinder,
|
||||
String ownerFqn,
|
||||
AccessorSummary accessor) {
|
||||
String nextType = null;
|
||||
ClassInstanceCreation fieldInitializerCic = null;
|
||||
|
||||
if (accessor.fieldName() != null) {
|
||||
fieldInitializerCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, accessor.fieldName());
|
||||
nextType = fieldInitializerFinder.resolveFieldTypeName(ownerFqn, accessor.fieldName());
|
||||
if (fieldInitializerCic != null) {
|
||||
nextType = AstUtils.extractSimpleTypeName(fieldInitializerCic.getType());
|
||||
}
|
||||
}
|
||||
|
||||
if (nextType == null) {
|
||||
TypeDeclaration ownerType = context.getTypeDeclaration(ownerFqn);
|
||||
MethodDeclaration getter = ownerType != null
|
||||
? context.findMethodDeclaration(ownerType, accessor.methodName(), true)
|
||||
: null;
|
||||
if (getter != null && getter.getReturnType2() != null) {
|
||||
nextType = getter.getReturnType2().toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (nextType == null) {
|
||||
return null;
|
||||
}
|
||||
return new GetterChainEdge(ownerFqn, accessor.methodName(), nextType, fieldInitializerCic);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Scan-time constants for indexed getters backed by fields with literal or enum initializers.
|
||||
*/
|
||||
public final class IndexedFieldConstantsIndex {
|
||||
|
||||
private static final IndexedFieldConstantsIndex EMPTY = new IndexedFieldConstantsIndex(Map.of(), 0);
|
||||
|
||||
private final Map<String, Map<String, List<String>>> byOwnerAndMethod;
|
||||
private final int entryCount;
|
||||
|
||||
public IndexedFieldConstantsIndex(Map<String, Map<String, List<String>>> byOwnerAndMethod, int entryCount) {
|
||||
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
|
||||
this.entryCount = entryCount;
|
||||
}
|
||||
|
||||
public static IndexedFieldConstantsIndex empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public Optional<List<String>> lookup(String ownerFqn, String methodName) {
|
||||
if (ownerFqn == null || methodName == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Map<String, List<String>> methods = byOwnerAndMethod.get(ownerFqn);
|
||||
if (methods == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
List<String> constants = methods.get(methodName);
|
||||
if (constants == null || constants.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(constants);
|
||||
}
|
||||
|
||||
public int entryCount() {
|
||||
return entryCount;
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, List<String>>> deepCopy(Map<String, Map<String, List<String>>> source) {
|
||||
Map<String, Map<String, List<String>>> copy = new HashMap<>();
|
||||
for (Map.Entry<String, Map<String, List<String>>> entry : source.entrySet()) {
|
||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public final class IndexedFieldConstantsIndexBuilder {
|
||||
|
||||
public IndexedFieldConstantsIndex build(CodebaseContext context, AccessorIndex accessorIndex) {
|
||||
if (context == null || accessorIndex == null) {
|
||||
return IndexedFieldConstantsIndex.empty();
|
||||
}
|
||||
|
||||
Map<String, Map<String, List<String>>> entries = new HashMap<>();
|
||||
int entryCount = 0;
|
||||
|
||||
for (Map.Entry<String, Map<String, AccessorSummary>> ownerEntry : accessorIndex.asMap().entrySet()) {
|
||||
String ownerFqn = ownerEntry.getKey();
|
||||
for (AccessorSummary accessor : ownerEntry.getValue().values()) {
|
||||
if (!accessor.isGetter() || accessor.fieldName() == null) {
|
||||
continue;
|
||||
}
|
||||
List<String> constants = ScanTimeAccessorConstants.resolve(
|
||||
context, accessorIndex, ownerFqn, accessor.methodName());
|
||||
if (constants.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
entries.computeIfAbsent(ownerFqn, ignored -> new HashMap<>())
|
||||
.put(accessor.methodName(), List.copyOf(constants));
|
||||
entryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Built indexed field-constants index: {} entries across {} types", entryCount, entries.size());
|
||||
return new IndexedFieldConstantsIndex(entries, entryCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Scan-time summary of how a getter on an interface or abstract type resolves across
|
||||
* its concrete implementations. Values are derived from static analysis only.
|
||||
*/
|
||||
public record PolymorphicAccessorEntry(
|
||||
String ownerFqn,
|
||||
String methodName,
|
||||
Optional<AccessorSummary> representativeImplAccessor,
|
||||
List<String> widenedReturnConstants,
|
||||
boolean staticallyComplete) {
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class PolymorphicAccessorIndex {
|
||||
|
||||
private static final PolymorphicAccessorIndex EMPTY = new PolymorphicAccessorIndex(Map.of(), 0);
|
||||
|
||||
private final Map<String, Map<String, PolymorphicAccessorEntry>> byOwnerAndMethod;
|
||||
private final int entryCount;
|
||||
|
||||
public PolymorphicAccessorIndex(
|
||||
Map<String, Map<String, PolymorphicAccessorEntry>> byOwnerAndMethod,
|
||||
int entryCount) {
|
||||
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
|
||||
this.entryCount = entryCount;
|
||||
}
|
||||
|
||||
public static PolymorphicAccessorIndex empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public Optional<PolymorphicAccessorEntry> lookup(String ownerFqn, String methodName) {
|
||||
if (ownerFqn == null || methodName == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Map<String, PolymorphicAccessorEntry> methods = byOwnerAndMethod.get(ownerFqn);
|
||||
if (methods == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(methods.get(methodName));
|
||||
}
|
||||
|
||||
public Optional<AccessorSummary> representativeImplAccessor(String ownerFqn, String methodName) {
|
||||
return lookup(ownerFqn, methodName)
|
||||
.flatMap(PolymorphicAccessorEntry::representativeImplAccessor);
|
||||
}
|
||||
|
||||
public Optional<List<String>> widenedReturnConstants(String ownerFqn, String methodName) {
|
||||
return lookup(ownerFqn, methodName)
|
||||
.filter(PolymorphicAccessorEntry::staticallyComplete)
|
||||
.map(PolymorphicAccessorEntry::widenedReturnConstants)
|
||||
.filter(values -> !values.isEmpty());
|
||||
}
|
||||
|
||||
public int entryCount() {
|
||||
return entryCount;
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, PolymorphicAccessorEntry>> deepCopy(
|
||||
Map<String, Map<String, PolymorphicAccessorEntry>> source) {
|
||||
Map<String, Map<String, PolymorphicAccessorEntry>> copy = new HashMap<>();
|
||||
for (Map.Entry<String, Map<String, PolymorphicAccessorEntry>> entry : source.entrySet()) {
|
||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.Modifier;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
public final class PolymorphicAccessorIndexBuilder {
|
||||
|
||||
public PolymorphicAccessorIndex build(CodebaseContext context, AccessorIndex accessorIndex) {
|
||||
if (context == null || accessorIndex == null) {
|
||||
return PolymorphicAccessorIndex.empty();
|
||||
}
|
||||
|
||||
Map<String, Map<String, PolymorphicAccessorEntry>> entries = new HashMap<>();
|
||||
int entryCount = 0;
|
||||
|
||||
for (String typeFqn : context.getIndexedTypeFqns()) {
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(typeFqn);
|
||||
if (!isPolymorphicType(typeDeclaration)) {
|
||||
continue;
|
||||
}
|
||||
List<String> implementations = context.getImplementations(typeFqn);
|
||||
if (implementations.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<String> methodNames = collectGetterMethodNames(context, accessorIndex, typeFqn, implementations);
|
||||
for (String methodName : methodNames) {
|
||||
PolymorphicAccessorEntry entry = buildEntry(
|
||||
context, accessorIndex, typeFqn, methodName, implementations);
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
entries.computeIfAbsent(typeFqn, ignored -> new HashMap<>()).put(methodName, entry);
|
||||
entryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Built polymorphic accessor index: {} entries across {} types", entryCount, entries.size());
|
||||
return new PolymorphicAccessorIndex(entries, entryCount);
|
||||
}
|
||||
|
||||
private static PolymorphicAccessorEntry buildEntry(
|
||||
CodebaseContext context,
|
||||
AccessorIndex accessorIndex,
|
||||
String ownerFqn,
|
||||
String methodName,
|
||||
List<String> implementations) {
|
||||
LinkedHashSet<String> widenedConstants = new LinkedHashSet<>();
|
||||
widenedConstants.addAll(ScanTimeAccessorConstants.resolve(context, accessorIndex, ownerFqn, methodName));
|
||||
|
||||
Optional<AccessorSummary> representative = findRepresentativeImplAccessor(accessorIndex, implementations, methodName);
|
||||
for (String implementationFqn : implementations) {
|
||||
widenedConstants.addAll(
|
||||
ScanTimeAccessorConstants.resolve(context, accessorIndex, implementationFqn, methodName));
|
||||
}
|
||||
|
||||
if (representative.isEmpty() && widenedConstants.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
boolean staticallyComplete = isStaticallyComplete(accessorIndex, methodName, implementations);
|
||||
return new PolymorphicAccessorEntry(
|
||||
ownerFqn,
|
||||
methodName,
|
||||
representative,
|
||||
List.copyOf(widenedConstants),
|
||||
staticallyComplete);
|
||||
}
|
||||
|
||||
private static boolean isStaticallyComplete(
|
||||
AccessorIndex accessorIndex,
|
||||
String methodName,
|
||||
List<String> implementations) {
|
||||
for (String implementationFqn : implementations) {
|
||||
if (!isConstantLikeAccessor(accessorIndex, implementationFqn, methodName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isConstantLikeAccessor(
|
||||
AccessorIndex accessorIndex,
|
||||
String typeFqn,
|
||||
String methodName) {
|
||||
Optional<AccessorSummary> accessor = accessorIndex.lookup(typeFqn, methodName);
|
||||
if (accessor.isEmpty() || !accessor.get().isGetter()) {
|
||||
return false;
|
||||
}
|
||||
return accessor.get().kind() == AccessorKind.CONSTANT_GETTER;
|
||||
}
|
||||
|
||||
private static Optional<AccessorSummary> findRepresentativeImplAccessor(
|
||||
AccessorIndex accessorIndex,
|
||||
List<String> implementations,
|
||||
String methodName) {
|
||||
for (String implementationFqn : implementations) {
|
||||
Optional<AccessorSummary> implAccessor = accessorIndex.lookup(implementationFqn, methodName);
|
||||
if (implAccessor.isPresent() && implAccessor.get().isGetter()) {
|
||||
return implAccessor;
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static Set<String> collectGetterMethodNames(
|
||||
CodebaseContext context,
|
||||
AccessorIndex accessorIndex,
|
||||
String ownerFqn,
|
||||
List<String> implementations) {
|
||||
Set<String> methodNames = new LinkedHashSet<>();
|
||||
for (AccessorSummary accessor : accessorIndex.accessorsForType(ownerFqn)) {
|
||||
if (accessor.isGetter()) {
|
||||
methodNames.add(accessor.methodName());
|
||||
}
|
||||
}
|
||||
TypeDeclaration ownerType = context.getTypeDeclaration(ownerFqn);
|
||||
if (ownerType != null) {
|
||||
for (MethodDeclaration method : ownerType.getMethods()) {
|
||||
String name = method.getName().getIdentifier();
|
||||
if (looksLikeGetterName(name)) {
|
||||
methodNames.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String implementationFqn : implementations) {
|
||||
for (AccessorSummary accessor : accessorIndex.accessorsForType(implementationFqn)) {
|
||||
if (accessor.isGetter()) {
|
||||
methodNames.add(accessor.methodName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return methodNames;
|
||||
}
|
||||
|
||||
private static boolean isPolymorphicType(TypeDeclaration typeDeclaration) {
|
||||
return typeDeclaration != null
|
||||
&& (typeDeclaration.isInterface()
|
||||
|| Modifier.isAbstract(typeDeclaration.getModifiers()));
|
||||
}
|
||||
|
||||
private static boolean looksLikeGetterName(String methodName) {
|
||||
return methodName.startsWith("get") || methodName.startsWith("is");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Scan-time expansion of interface/abstract method calls to concrete implementation method FQNs.
|
||||
*/
|
||||
public final class PolymorphicMethodExpansionIndex {
|
||||
|
||||
private static final PolymorphicMethodExpansionIndex EMPTY =
|
||||
new PolymorphicMethodExpansionIndex(Map.of(), 0);
|
||||
|
||||
private final Map<String, Map<String, List<String>>> byOwnerAndMethod;
|
||||
private final int entryCount;
|
||||
|
||||
public PolymorphicMethodExpansionIndex(
|
||||
Map<String, Map<String, List<String>>> byOwnerAndMethod,
|
||||
int entryCount) {
|
||||
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
|
||||
this.entryCount = entryCount;
|
||||
}
|
||||
|
||||
public static PolymorphicMethodExpansionIndex empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public Optional<List<String>> lookup(String ownerFqn, String methodName) {
|
||||
if (ownerFqn == null || methodName == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Map<String, List<String>> methods = byOwnerAndMethod.get(ownerFqn);
|
||||
if (methods == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
List<String> expanded = methods.get(methodName);
|
||||
if (expanded == null || expanded.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(expanded);
|
||||
}
|
||||
|
||||
public int entryCount() {
|
||||
return entryCount;
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, List<String>>> deepCopy(
|
||||
Map<String, Map<String, List<String>>> source) {
|
||||
Map<String, Map<String, List<String>>> copy = new HashMap<>();
|
||||
for (Map.Entry<String, Map<String, List<String>>> entry : source.entrySet()) {
|
||||
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.Modifier;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
public final class PolymorphicMethodExpansionIndexBuilder {
|
||||
|
||||
public PolymorphicMethodExpansionIndex build(CodebaseContext context) {
|
||||
if (context == null) {
|
||||
return PolymorphicMethodExpansionIndex.empty();
|
||||
}
|
||||
|
||||
Map<String, Map<String, List<String>>> entries = new HashMap<>();
|
||||
int entryCount = 0;
|
||||
|
||||
for (String typeFqn : context.getIndexedTypeFqns()) {
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(typeFqn);
|
||||
if (!isPolymorphicType(typeDeclaration)) {
|
||||
continue;
|
||||
}
|
||||
List<String> implementations = context.getImplementations(typeFqn);
|
||||
if (implementations.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<String> methodNames = collectMethodNames(context, typeDeclaration, implementations);
|
||||
for (String methodName : methodNames) {
|
||||
List<String> expanded = new ArrayList<>();
|
||||
for (String implementationFqn : implementations) {
|
||||
expanded.add(implementationFqn + "." + methodName);
|
||||
}
|
||||
entries.computeIfAbsent(typeFqn, ignored -> new HashMap<>())
|
||||
.put(methodName, List.copyOf(expanded));
|
||||
entryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Built polymorphic method expansion index: {} entries across {} types",
|
||||
entryCount, entries.size());
|
||||
return new PolymorphicMethodExpansionIndex(entries, entryCount);
|
||||
}
|
||||
|
||||
private static Set<String> collectMethodNames(
|
||||
CodebaseContext context,
|
||||
TypeDeclaration ownerType,
|
||||
List<String> implementations) {
|
||||
Set<String> methodNames = new LinkedHashSet<>();
|
||||
if (ownerType != null) {
|
||||
for (var method : ownerType.getMethods()) {
|
||||
methodNames.add(method.getName().getIdentifier());
|
||||
}
|
||||
}
|
||||
for (String implementationFqn : implementations) {
|
||||
TypeDeclaration implementationType = context.getTypeDeclaration(implementationFqn);
|
||||
if (implementationType == null) {
|
||||
continue;
|
||||
}
|
||||
for (var method : implementationType.getMethods()) {
|
||||
methodNames.add(method.getName().getIdentifier());
|
||||
}
|
||||
}
|
||||
return methodNames;
|
||||
}
|
||||
|
||||
private static boolean isPolymorphicType(TypeDeclaration typeDeclaration) {
|
||||
return typeDeclaration != null
|
||||
&& (typeDeclaration.isInterface()
|
||||
|| Modifier.isAbstract(typeDeclaration.getModifiers()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Resolves accessor return constants using only scan-time information: indexed accessors,
|
||||
* literal method bodies, and field initializers. No dataflow or constructor tracing.
|
||||
*/
|
||||
public final class ScanTimeAccessorConstants {
|
||||
|
||||
private ScanTimeAccessorConstants() {
|
||||
}
|
||||
|
||||
public static List<String> resolve(
|
||||
CodebaseContext context,
|
||||
AccessorIndex accessorIndex,
|
||||
String ownerFqn,
|
||||
String methodName) {
|
||||
if (context == null || accessorIndex == null || ownerFqn == null || methodName == null) {
|
||||
return List.of();
|
||||
}
|
||||
Set<String> deduped = new LinkedHashSet<>();
|
||||
Optional<AccessorSummary> indexed = accessorIndex.lookup(ownerFqn, methodName);
|
||||
if (indexed.isPresent() && indexed.get().isGetter()) {
|
||||
if (indexed.get().kind() == AccessorKind.CONSTANT_GETTER) {
|
||||
deduped.addAll(extractReturnConstants(context, ownerFqn, methodName));
|
||||
} else if (indexed.get().fieldName() != null) {
|
||||
deduped.addAll(extractFieldInitializerConstants(
|
||||
context, ownerFqn, indexed.get().fieldName()));
|
||||
}
|
||||
}
|
||||
if (deduped.isEmpty()) {
|
||||
deduped.addAll(extractReturnConstants(context, ownerFqn, methodName));
|
||||
}
|
||||
return List.copyOf(deduped);
|
||||
}
|
||||
|
||||
private static List<String> extractReturnConstants(
|
||||
CodebaseContext context,
|
||||
String ownerFqn,
|
||||
String methodName) {
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> constants = new ArrayList<>();
|
||||
ConstantResolver constantResolver = context.getConstantResolver();
|
||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
Expression expression = node.getExpression();
|
||||
if (expression != null) {
|
||||
addResolvedExpression(constantResolver, context, expression, constants);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return constants;
|
||||
}
|
||||
|
||||
private static List<String> extractFieldInitializerConstants(
|
||||
CodebaseContext context,
|
||||
String ownerFqn,
|
||||
String fieldName) {
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> constants = new ArrayList<>();
|
||||
collectFieldInitializerConstants(context, typeDeclaration, fieldName, constants);
|
||||
return constants;
|
||||
}
|
||||
|
||||
private static void collectFieldInitializerConstants(
|
||||
CodebaseContext context,
|
||||
TypeDeclaration typeDeclaration,
|
||||
String fieldName,
|
||||
List<String> constants) {
|
||||
ConstantResolver constantResolver = context.getConstantResolver();
|
||||
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
||||
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
||||
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
||||
&& fragment.getName().getIdentifier().equals(fieldName)
|
||||
&& fragment.getInitializer() != null) {
|
||||
addResolvedExpression(
|
||||
constantResolver, context, fragment.getInitializer(), constants);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (constants.isEmpty()) {
|
||||
String superFqn = context.getSuperclassFqn(typeDeclaration);
|
||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
||||
TypeDeclaration superType = context.getTypeDeclaration(superFqn);
|
||||
if (superType != null) {
|
||||
collectFieldInitializerConstants(context, superType, fieldName, constants);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addResolvedExpression(
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context,
|
||||
Expression expression,
|
||||
List<String> constants) {
|
||||
if (constantResolver == null || expression == null) {
|
||||
return;
|
||||
}
|
||||
String resolved = constantResolver.resolve(expression, context);
|
||||
if (resolved == null) {
|
||||
return;
|
||||
}
|
||||
if (resolved.startsWith("ENUM_SET:")) {
|
||||
for (String value : resolved.substring(9).split(",")) {
|
||||
addUnique(constants, parseEnumSetElement(value));
|
||||
}
|
||||
} else if (resolved.startsWith("MAP:")) {
|
||||
List<String> mapValues = ConstantResolver.decodeMapLiteralValues(resolved);
|
||||
if (mapValues != null) {
|
||||
for (String mapValue : mapValues) {
|
||||
addUnique(constants, mapValue);
|
||||
}
|
||||
}
|
||||
} else if (resolved.startsWith("ARRAY:")) {
|
||||
for (String arrayValue : resolved.substring(6).split("\\|", -1)) {
|
||||
addUnique(constants, arrayValue);
|
||||
}
|
||||
} else {
|
||||
addUnique(constants, resolved);
|
||||
}
|
||||
}
|
||||
|
||||
private static String parseEnumSetElement(String value) {
|
||||
int lastDot = value.lastIndexOf('.');
|
||||
int secondLastDot = lastDot > 0 ? value.lastIndexOf('.', lastDot - 1) : -1;
|
||||
return secondLastDot >= 0 ? value.substring(secondLastDot + 1) : value;
|
||||
}
|
||||
|
||||
private static void addUnique(List<String> constants, String value) {
|
||||
if (value != null && !constants.contains(value)) {
|
||||
constants.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorKind;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.GetterChainEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
@@ -46,6 +47,7 @@ public final class AccessorResolver {
|
||||
private final ConstantResolver constantResolver;
|
||||
private final FieldInitializerFinder fieldInitializerFinder;
|
||||
private final Map<String, List<String>> sessionCache = new HashMap<>();
|
||||
private final Map<String, Optional<AccessorSummary>> indexedGetterCache = new HashMap<>();
|
||||
|
||||
public AccessorResolver(
|
||||
CodebaseContext context,
|
||||
@@ -63,6 +65,16 @@ public final class AccessorResolver {
|
||||
if (className == null || className.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String cacheKey = className + "#" + methodName;
|
||||
if (indexedGetterCache.containsKey(cacheKey)) {
|
||||
return indexedGetterCache.get(cacheKey);
|
||||
}
|
||||
Optional<AccessorSummary> resolved = lookupIndexedGetterInternal(className, methodName);
|
||||
indexedGetterCache.put(cacheKey, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private Optional<AccessorSummary> lookupIndexedGetterInternal(String className, String methodName) {
|
||||
Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
|
||||
if (direct.isPresent()) {
|
||||
return direct;
|
||||
@@ -78,9 +90,14 @@ public final class AccessorResolver {
|
||||
}
|
||||
}
|
||||
List<String> implementations = context.getImplementations(className);
|
||||
if (implementations == null) {
|
||||
if (implementations == null || implementations.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<AccessorSummary> precomputed = context.getPolymorphicAccessorIndex()
|
||||
.representativeImplAccessor(className, methodName);
|
||||
if (precomputed.isPresent()) {
|
||||
return precomputed;
|
||||
}
|
||||
for (String implementation : implementations) {
|
||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
||||
if (implAccessor.isPresent()) {
|
||||
@@ -114,7 +131,7 @@ public final class AccessorResolver {
|
||||
}
|
||||
|
||||
List<String> resolved = resolveInternal(ownerFqn, methodName, receiverContext, budget, visited);
|
||||
if (!resolved.isEmpty()) {
|
||||
if (!resolved.isEmpty() || isStaticCacheable(receiverContext)) {
|
||||
List<String> cachedCopy = List.copyOf(resolved);
|
||||
sessionCache.put(cacheKey, cachedCopy);
|
||||
return cachedCopy;
|
||||
@@ -127,6 +144,12 @@ public final class AccessorResolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<GetterChainEdge> precomputed = context.getGetterChainIndex().lookup(ownerFqn, getterName);
|
||||
if (precomputed.isPresent()) {
|
||||
GetterChainEdge edge = precomputed.get();
|
||||
return new GetterChainStep(edge.nextTypeFqn(), edge.fieldInitializerCic());
|
||||
}
|
||||
|
||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, getterName);
|
||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
||||
AccessorSummary accessor = indexedAccessor.get();
|
||||
@@ -165,8 +188,20 @@ public final class AccessorResolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isStaticCacheable(ReceiverContext receiverContext) {
|
||||
if (receiverContext == null) {
|
||||
return true;
|
||||
}
|
||||
if (receiverContext.cic() != null) {
|
||||
return false;
|
||||
}
|
||||
Map<String, String> fieldValues = receiverContext.fieldValues();
|
||||
return fieldValues == null || fieldValues.isEmpty();
|
||||
}
|
||||
|
||||
public void clearSessionCache() {
|
||||
sessionCache.clear();
|
||||
indexedGetterCache.clear();
|
||||
}
|
||||
|
||||
private List<String> resolveInternal(
|
||||
@@ -184,25 +219,42 @@ public final class AccessorResolver {
|
||||
|| org.eclipse.jdt.core.dom.Modifier.isAbstract(ownerType.getModifiers());
|
||||
|
||||
if (widenToImplementations) {
|
||||
List<String> widened = new ArrayList<>();
|
||||
List<String> impls = context.getImplementations(ownerFqn);
|
||||
if (impls != null) {
|
||||
for (String implFqn : impls) {
|
||||
TypeDeclaration implType = context.getTypeDeclaration(implFqn);
|
||||
widened.addAll(resolve(
|
||||
implFqn,
|
||||
List<String> results = new ArrayList<>();
|
||||
|
||||
if (receiverContext != null && receiverContext.cic() != null) {
|
||||
String cicOwnerFqn = resolveCicOwnerFqn(receiverContext);
|
||||
if (cicOwnerFqn != null) {
|
||||
TypeDeclaration cicOwnerType = context.getTypeDeclaration(
|
||||
cicOwnerFqn, receiverContext.contextCu());
|
||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(cicOwnerFqn, methodName);
|
||||
ReceiverContext cicReceiverContext = new ReceiverContext(
|
||||
cicOwnerType,
|
||||
receiverContext.cic(),
|
||||
receiverContext.contextCu(),
|
||||
receiverContext.anonymousGetter(),
|
||||
receiverContext.fieldValues());
|
||||
List<String> cicValues = resolveWithCic(
|
||||
cicOwnerFqn,
|
||||
methodName,
|
||||
new ReceiverContext(implType, null,
|
||||
receiverContext != null ? receiverContext.contextCu() : null,
|
||||
false,
|
||||
receiverContext != null ? receiverContext.fieldValues() : null),
|
||||
budget,
|
||||
visited));
|
||||
cicReceiverContext,
|
||||
indexedAccessor.orElse(null),
|
||||
visited);
|
||||
if (!cicValues.isEmpty()) {
|
||||
return deduplicatePreservingOrder(cicValues);
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
if (!widened.isEmpty()) {
|
||||
return deduplicatePreservingOrder(widened);
|
||||
|
||||
if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
|
||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
||||
results.addAll(constantExtractor.resolveMethodReturnConstant(
|
||||
ownerFqn, methodName, 0, visited, contextCu, budget));
|
||||
}
|
||||
if (!results.isEmpty()) {
|
||||
return deduplicatePreservingOrder(results);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, methodName);
|
||||
@@ -262,8 +314,11 @@ public final class AccessorResolver {
|
||||
|
||||
if (constantExtractor != null) {
|
||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
||||
String requestingTypeFqn = receiverContext != null && receiverContext.ownerType() != null
|
||||
? context.getFqn(receiverContext.ownerType())
|
||||
: accessor.ownerFqn();
|
||||
List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
||||
accessor, contextCu, visited);
|
||||
accessor, requestingTypeFqn, contextCu, visited);
|
||||
if (!fieldConstants.isEmpty()) {
|
||||
return fieldConstants;
|
||||
}
|
||||
@@ -335,7 +390,7 @@ public final class AccessorResolver {
|
||||
}
|
||||
if (constantExtractor != null) {
|
||||
List<String> indexedConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
||||
indexedAccessor, receiverContext.contextCu(), visited);
|
||||
indexedAccessor, ownerFqn, receiverContext.contextCu(), visited);
|
||||
if (!indexedConstants.isEmpty()) {
|
||||
return indexedConstants;
|
||||
}
|
||||
@@ -400,6 +455,28 @@ public final class AccessorResolver {
|
||||
return accessorOpt.get().fieldName();
|
||||
}
|
||||
|
||||
private String resolveCicOwnerFqn(ReceiverContext receiverContext) {
|
||||
if (receiverContext == null || receiverContext.cic() == null) {
|
||||
return null;
|
||||
}
|
||||
ClassInstanceCreation cic = receiverContext.cic();
|
||||
CompilationUnit contextCu = receiverContext.contextCu();
|
||||
String simpleName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||
TypeDeclaration cicType = contextCu != null
|
||||
? context.getTypeDeclaration(simpleName, contextCu)
|
||||
: null;
|
||||
if (cicType == null) {
|
||||
cicType = context.getTypeDeclaration(simpleName);
|
||||
}
|
||||
if (cicType != null) {
|
||||
return context.getFqn(cicType);
|
||||
}
|
||||
if (cic.resolveTypeBinding() != null) {
|
||||
return cic.resolveTypeBinding().getErasure().getQualifiedName();
|
||||
}
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
private static List<String> deduplicatePreservingOrder(List<String> values) {
|
||||
List<String> deduped = new ArrayList<>();
|
||||
for (String value : values) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Memoizes polymorphic class compatibility checks used during call-graph path matching.
|
||||
*/
|
||||
public final class ClassCompatibilityCache {
|
||||
|
||||
private final Map<Map.Entry<String, String>, Boolean> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public boolean areClassesCompatible(String classNeighbor, String classTarget, ClassCompatibilityChecker checker) {
|
||||
if (classNeighbor == null || classTarget == null) {
|
||||
return false;
|
||||
}
|
||||
if (classNeighbor.equals(classTarget)) {
|
||||
return true;
|
||||
}
|
||||
Map.Entry<String, String> key = cacheKey(classNeighbor, classTarget);
|
||||
return cache.computeIfAbsent(key, ignored -> checker.check(classNeighbor, classTarget));
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
private static Map.Entry<String, String> cacheKey(String classNeighbor, String classTarget) {
|
||||
if (classNeighbor.compareTo(classTarget) <= 0) {
|
||||
return new AbstractMap.SimpleImmutableEntry<>(classNeighbor, classTarget);
|
||||
}
|
||||
return new AbstractMap.SimpleImmutableEntry<>(classTarget, classNeighbor);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ClassCompatibilityChecker {
|
||||
boolean check(String classNeighbor, String classTarget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.Block;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.LambdaExpression;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||
import org.eclipse.jdt.core.dom.SimpleName;
|
||||
import org.eclipse.jdt.core.dom.ASTNode;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Extracts terminal payload literals from reactive factory chains ({@code Mono.just}, nested {@code flatMap}).
|
||||
*/
|
||||
public final class ReactiveExpressionSupport {
|
||||
|
||||
private static final Set<String> REACTIVE_FACTORY_METHODS = Set.of("just", "withPayload", "success");
|
||||
|
||||
private ReactiveExpressionSupport() {
|
||||
}
|
||||
|
||||
public static String extractPayload(Expression expression, ConstantResolver constantResolver, CodebaseContext context) {
|
||||
return extractPayload(expression, null, constantResolver, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites {@code p.getEvent()} inside a {@code flatMap(p -> ...)} lambda to {@code payload.getEvent()}
|
||||
* when {@code p} is fed by {@code Mono.just(payload)} (or a chained reactive source).
|
||||
*/
|
||||
public static String remapLambdaParameterGetter(
|
||||
Expression eventExpr,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (!(eventExpr instanceof MethodInvocation getterCall)) {
|
||||
return null;
|
||||
}
|
||||
if (!(getterCall.getExpression() instanceof SimpleName paramName)) {
|
||||
return null;
|
||||
}
|
||||
LambdaExpression lambda = findEnclosingLambda(getterCall);
|
||||
if (lambda == null) {
|
||||
return null;
|
||||
}
|
||||
MethodInvocation flatMapCall = findEnclosingFlatMap(lambda);
|
||||
if (flatMapCall == null) {
|
||||
return null;
|
||||
}
|
||||
String mappedReceiver = mapLambdaParameterToSource(
|
||||
lambda,
|
||||
paramName.getIdentifier(),
|
||||
flatMapCall.getExpression(),
|
||||
constantResolver,
|
||||
context);
|
||||
if (mappedReceiver == null) {
|
||||
return null;
|
||||
}
|
||||
return mappedReceiver + "." + getterCall.getName().getIdentifier() + "()";
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #remapLambdaParameterGetter(Expression, ConstantResolver, CodebaseContext)} but locates
|
||||
* the getter in a real method body when {@code expressionText} came from a detached parse tree.
|
||||
*/
|
||||
public static String remapLambdaParameterGetterInMethod(
|
||||
String expressionText,
|
||||
String methodFqn,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
MethodInvocation getter = findMethodInvocationInMethod(methodFqn, expressionText, context);
|
||||
if (getter == null) {
|
||||
return null;
|
||||
}
|
||||
String remapped = remapLambdaParameterGetter(getter, constantResolver, context);
|
||||
if (remapped != null) {
|
||||
return remapped;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MethodInvocation findMethodInvocationInMethod(
|
||||
String methodFqn,
|
||||
String expressionText,
|
||||
CodebaseContext context) {
|
||||
if (methodFqn == null || expressionText == null || !methodFqn.contains(".")) {
|
||||
return null;
|
||||
}
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
org.eclipse.jdt.core.dom.MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md == null || md.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
final MethodInvocation[] match = new MethodInvocation[1];
|
||||
md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if (expressionText.equals(node.toString())) {
|
||||
match[0] = node;
|
||||
return false;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return match[0];
|
||||
}
|
||||
|
||||
private static LambdaExpression findEnclosingLambda(ASTNode node) {
|
||||
ASTNode current = node.getParent();
|
||||
while (current != null) {
|
||||
if (current instanceof LambdaExpression lambda) {
|
||||
return lambda;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MethodInvocation findEnclosingFlatMap(ASTNode node) {
|
||||
ASTNode current = node.getParent();
|
||||
while (current != null) {
|
||||
if (current instanceof MethodInvocation mi && "flatMap".equals(mi.getName().getIdentifier())) {
|
||||
return mi;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String extractPayload(
|
||||
Expression expression,
|
||||
Expression flatMapReceiver,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (expression == null) {
|
||||
return null;
|
||||
}
|
||||
if (expression instanceof MethodInvocation mi) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
if ("flatMap".equals(methodName) && !mi.arguments().isEmpty()) {
|
||||
String fromArgument = extractFlatMapArgumentPayload(
|
||||
(Expression) mi.arguments().get(0), mi.getExpression(), constantResolver, context);
|
||||
if (fromArgument != null) {
|
||||
return fromArgument;
|
||||
}
|
||||
}
|
||||
if (REACTIVE_FACTORY_METHODS.contains(methodName) && !mi.arguments().isEmpty()) {
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
if (constantResolver != null && context != null) {
|
||||
String resolved = constantResolver.resolve(arg, context);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return arg.toString();
|
||||
}
|
||||
if (mi.getExpression() != null) {
|
||||
return extractPayload(mi.getExpression(), flatMapReceiver, constantResolver, context);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String extractFlatMapArgumentPayload(
|
||||
Expression argument,
|
||||
Expression flatMapReceiver,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (argument instanceof LambdaExpression lambda) {
|
||||
Expression bodyExpression = lambdaBodyExpression(lambda);
|
||||
if (bodyExpression != null) {
|
||||
String lambdaPayload = extractLambdaBodyPayload(
|
||||
bodyExpression, lambda, flatMapReceiver, constantResolver, context);
|
||||
if (lambdaPayload != null) {
|
||||
return lambdaPayload;
|
||||
}
|
||||
return extractPayload(bodyExpression, flatMapReceiver, constantResolver, context);
|
||||
}
|
||||
}
|
||||
return extractPayload(argument, flatMapReceiver, constantResolver, context);
|
||||
}
|
||||
|
||||
private static String extractLambdaBodyPayload(
|
||||
Expression bodyExpression,
|
||||
LambdaExpression lambda,
|
||||
Expression flatMapReceiver,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (!(bodyExpression instanceof MethodInvocation factoryCall)) {
|
||||
return null;
|
||||
}
|
||||
if (!REACTIVE_FACTORY_METHODS.contains(factoryCall.getName().getIdentifier())
|
||||
|| factoryCall.arguments().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Expression factoryArg = (Expression) factoryCall.arguments().get(0);
|
||||
if (!(factoryArg instanceof MethodInvocation getterCall)) {
|
||||
return null;
|
||||
}
|
||||
String getterSuffix = "." + getterCall.getName().getIdentifier() + "()";
|
||||
Expression getterReceiver = getterCall.getExpression();
|
||||
if (getterReceiver instanceof SimpleName paramName) {
|
||||
String mappedReceiver = mapLambdaParameterToSource(
|
||||
lambda, paramName.getIdentifier(), flatMapReceiver, constantResolver, context);
|
||||
if (mappedReceiver != null) {
|
||||
return mappedReceiver + getterSuffix;
|
||||
}
|
||||
}
|
||||
if (getterReceiver != null) {
|
||||
return getterReceiver.toString() + getterSuffix;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String mapLambdaParameterToSource(
|
||||
LambdaExpression lambda,
|
||||
String paramName,
|
||||
Expression flatMapReceiver,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (lambda.parameters().size() != 1) {
|
||||
return null;
|
||||
}
|
||||
Object parameter = lambda.parameters().get(0);
|
||||
if (!(parameter instanceof org.eclipse.jdt.core.dom.VariableDeclaration variableDeclaration)) {
|
||||
return null;
|
||||
}
|
||||
if (!paramName.equals(variableDeclaration.getName().getIdentifier())) {
|
||||
return null;
|
||||
}
|
||||
if (flatMapReceiver instanceof MethodInvocation receiverFlatMap
|
||||
&& "flatMap".equals(receiverFlatMap.getName().getIdentifier())
|
||||
&& !receiverFlatMap.arguments().isEmpty()
|
||||
&& receiverFlatMap.arguments().get(0) instanceof LambdaExpression feederLambda
|
||||
&& feederLambda != lambda) {
|
||||
Expression feederBody = lambdaBodyExpression(feederLambda);
|
||||
if (feederBody != null) {
|
||||
String feederPayload = extractLambdaBodyPayload(
|
||||
feederBody, feederLambda, receiverFlatMap.getExpression(), constantResolver, context);
|
||||
if (feederPayload != null) {
|
||||
return feederPayload;
|
||||
}
|
||||
String extracted = extractPayload(
|
||||
feederBody, receiverFlatMap.getExpression(), constantResolver, context);
|
||||
if (extracted != null) {
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
}
|
||||
Expression source = peelJustArgument(flatMapReceiver);
|
||||
if (source instanceof MethodInvocation getterMi
|
||||
&& getterMi.getExpression() instanceof SimpleName innerParamName) {
|
||||
LambdaExpression outerLambda = findEnclosingLambda(lambda);
|
||||
if (outerLambda != null && outerLambda != lambda) {
|
||||
MethodInvocation outerFlatMap = findEnclosingFlatMap(outerLambda);
|
||||
if (outerFlatMap != null) {
|
||||
String mappedBase = mapLambdaParameterToSource(
|
||||
outerLambda,
|
||||
innerParamName.getIdentifier(),
|
||||
outerFlatMap.getExpression(),
|
||||
constantResolver,
|
||||
context);
|
||||
if (mappedBase != null) {
|
||||
return mappedBase + "." + getterMi.getName().getIdentifier() + "()";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (source != null) {
|
||||
if (constantResolver != null && context != null) {
|
||||
String resolved = constantResolver.resolve(source, context);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return source.toString();
|
||||
}
|
||||
return extractPayload(flatMapReceiver, null, constantResolver, context);
|
||||
}
|
||||
|
||||
private static Expression peelJustArgument(Expression expression) {
|
||||
if (expression instanceof MethodInvocation mi
|
||||
&& REACTIVE_FACTORY_METHODS.contains(mi.getName().getIdentifier())
|
||||
&& !mi.arguments().isEmpty()) {
|
||||
return (Expression) mi.arguments().get(0);
|
||||
}
|
||||
if (expression instanceof MethodInvocation mi && mi.getExpression() != null) {
|
||||
return peelJustArgument(mi.getExpression());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression lambdaBodyExpression(LambdaExpression lambda) {
|
||||
if (lambda.getBody() instanceof Expression bodyExpression) {
|
||||
return bodyExpression;
|
||||
}
|
||||
if (lambda.getBody() instanceof Block block) {
|
||||
for (Object statement : block.statements()) {
|
||||
if (statement instanceof ReturnStatement returnStatement
|
||||
&& returnStatement.getExpression() != null) {
|
||||
return returnStatement.getExpression();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import java.util.Set;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class ConstantResolver {
|
||||
@@ -19,6 +20,36 @@ public class ConstantResolver {
|
||||
return resolveInternal(expr, context, new HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves {@code map.get(key)} when the map receiver and key are statically known.
|
||||
*/
|
||||
public String resolveKeyedMapLookup(MethodInvocation getCall, CodebaseContext context) {
|
||||
if (getCall == null || getCall.getExpression() == null || getCall.arguments().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String methodName = getCall.getName().getIdentifier();
|
||||
if (!"get".equals(methodName) && !"getOrDefault".equals(methodName)) {
|
||||
return null;
|
||||
}
|
||||
return LiteralExpressionSupport.resolveMapGet(
|
||||
getCall, context, new HashSet<>(), (e, v) -> resolveInternal(e, context, v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a value from an encoded {@code MAP:...} literal or map-typed expression at {@code key}.
|
||||
*/
|
||||
public String resolveKeyedMapLookup(Expression mapExpr, String key, CodebaseContext context) {
|
||||
if (mapExpr == null || key == null) {
|
||||
return null;
|
||||
}
|
||||
String encoded = resolveInternal(mapExpr, context, new HashSet<>());
|
||||
if (encoded == null || !encoded.startsWith("MAP:")) {
|
||||
return null;
|
||||
}
|
||||
Map<String, String> entries = LiteralExpressionSupport.parseMapLiteral(encoded);
|
||||
return entries != null ? entries.get(key) : null;
|
||||
}
|
||||
|
||||
public static List<String> decodeMapLiteralValues(String encoded) {
|
||||
return LiteralExpressionSupport.mapLiteralValues(encoded);
|
||||
}
|
||||
@@ -818,12 +849,13 @@ public class ConstantResolver {
|
||||
}
|
||||
}
|
||||
|
||||
td = typeFromCurrentCallPath(context);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
String result = resolveFieldInType(td, sn.getIdentifier(), fqn, context, visited);
|
||||
if (result != null) {
|
||||
return result;
|
||||
for (String classFqn : classesFromCurrentCallPath()) {
|
||||
TypeDeclaration pathTd = context.getTypeDeclaration(classFqn);
|
||||
if (pathTd != null) {
|
||||
String result = resolveFieldInType(pathTd, sn.getIdentifier(), classFqn, context, visited);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,21 +933,35 @@ public class ConstantResolver {
|
||||
}
|
||||
|
||||
private TypeDeclaration typeFromCurrentCallPath(CodebaseContext context) {
|
||||
String entryClass = getCurrentCallPathEntryClass();
|
||||
if (entryClass == null) {
|
||||
List<String> classes = classesFromCurrentCallPath();
|
||||
if (classes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return context.getTypeDeclaration(entryClass);
|
||||
return context.getTypeDeclaration(classes.get(0));
|
||||
}
|
||||
|
||||
private List<String> classesFromCurrentCallPath() {
|
||||
List<String> path = JdtDataFlowModel.getCurrentPath();
|
||||
if (path == null || path.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> classes = new ArrayList<>();
|
||||
for (int i = path.size() - 1; i >= 0; i--) {
|
||||
String methodFqn = path.get(i);
|
||||
int dot = methodFqn.lastIndexOf('.');
|
||||
if (dot > 0) {
|
||||
String className = methodFqn.substring(0, dot);
|
||||
if (!classes.contains(className)) {
|
||||
classes.add(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
private String getCurrentCallPathEntryClass() {
|
||||
List<String> path = JdtDataFlowModel.getCurrentPath();
|
||||
if (path == null || path.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String entryMethod = path.get(0);
|
||||
int dot = entryMethod.lastIndexOf('.');
|
||||
return dot > 0 ? entryMethod.substring(0, dot) : null;
|
||||
List<String> classes = classesFromCurrentCallPath();
|
||||
return classes.isEmpty() ? null : classes.get(classes.size() - 1);
|
||||
}
|
||||
|
||||
private String resolveFieldInType(TypeDeclaration td, String fieldName, String typeFqn, CodebaseContext context, Set<String> visited) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,780 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class CallGraphBuilder {
|
||||
private final CodebaseContext context;
|
||||
private final ConstantResolver constantResolver;
|
||||
private String currentMethodFqn;
|
||||
private Map<String, List<CallEdge>> graph;
|
||||
|
||||
@lombok.Data
|
||||
private static class CallEdge {
|
||||
private final String targetMethod;
|
||||
private final List<String> arguments;
|
||||
}
|
||||
|
||||
public CallGraphBuilder(CodebaseContext context) {
|
||||
this.context = context;
|
||||
this.constantResolver = new ConstantResolver();
|
||||
}
|
||||
|
||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
||||
List<CallChain> chains = new ArrayList<>();
|
||||
|
||||
for (EntryPoint ep : entryPoints) {
|
||||
String startMethod = ep.getClassName() + "." + ep.getMethodName();
|
||||
boolean foundAny = false;
|
||||
for (TriggerPoint tp : triggers) {
|
||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
|
||||
if (path != null) {
|
||||
foundAny = true;
|
||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
||||
String contextMachineId = extractContextMachineId(path, callGraph);
|
||||
chains.add(CallChain.builder()
|
||||
.entryPoint(ep)
|
||||
.triggerPoint(resolvedTp)
|
||||
.methodChain(path)
|
||||
.contextMachineId(contextMachineId)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
if (!foundAny && log.isDebugEnabled()) {
|
||||
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}", ep.getName(), startMethod, callGraph.keySet());
|
||||
}
|
||||
}
|
||||
return chains;
|
||||
}
|
||||
|
||||
private TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
if (path.size() < 2) return tp;
|
||||
|
||||
String event = tp.getEvent();
|
||||
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
|
||||
|
||||
String currentParamName = event;
|
||||
String resolvedValue = event;
|
||||
String methodSuffix = "";
|
||||
|
||||
// Extract method calls like .getType() so we can trace the base parameter
|
||||
int dotIndex = currentParamName.indexOf('.');
|
||||
if (dotIndex > 0 && dotIndex + 1 < currentParamName.length()) {
|
||||
char nextChar = currentParamName.charAt(dotIndex + 1);
|
||||
if (Character.isLowerCase(nextChar)) {
|
||||
methodSuffix = currentParamName.substring(dotIndex);
|
||||
currentParamName = currentParamName.substring(0, dotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk backwards up the call chain
|
||||
for (int i = path.size() - 1; i > 0; i--) {
|
||||
String target = path.get(i);
|
||||
String caller = path.get(i - 1);
|
||||
|
||||
// Find parameter index in target method
|
||||
int paramIndex = getParameterIndex(target, currentParamName);
|
||||
if (paramIndex < 0) {
|
||||
// Not a parameter. Maybe it's a local variable initialized from a parameter or method call?
|
||||
String tracedVar = traceLocalVariable(target, currentParamName);
|
||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||
// Extract method calls like .getType() from the traced variable
|
||||
int dotIdx = tracedVar.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
|
||||
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
|
||||
tracedVar = tracedVar.substring(0, dotIdx);
|
||||
}
|
||||
currentParamName = tracedVar;
|
||||
resolvedValue = tracedVar + methodSuffix;
|
||||
paramIndex = getParameterIndex(target, currentParamName);
|
||||
}
|
||||
}
|
||||
if (paramIndex < 0) {
|
||||
break; // Parameter name changed or not found, stop tracing
|
||||
}
|
||||
|
||||
// Find the edge from caller to target to get the argument passed
|
||||
List<CallEdge> edges = callGraph.get(caller);
|
||||
boolean found = false;
|
||||
if (edges != null) {
|
||||
for (CallEdge edge : edges) {
|
||||
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||
if (paramIndex < edge.getArguments().size()) {
|
||||
String arg = edge.getArguments().get(paramIndex);
|
||||
if (arg != null) {
|
||||
// If the argument passed has a method call, extract it
|
||||
int dotIdx = arg.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx + 1 < arg.length() && Character.isLowerCase(arg.charAt(dotIdx + 1))) {
|
||||
methodSuffix = arg.substring(dotIdx) + methodSuffix;
|
||||
arg = arg.substring(0, dotIdx);
|
||||
}
|
||||
currentParamName = arg;
|
||||
resolvedValue = arg + methodSuffix;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) break; // Could not map argument
|
||||
}
|
||||
|
||||
// Final check on the entry method
|
||||
String entryMethod = path.get(0);
|
||||
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
|
||||
if (entryParamIndex < 0) {
|
||||
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
|
||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||
int dotIdx = tracedVar.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx + 1 < tracedVar.length() && Character.isLowerCase(tracedVar.charAt(dotIdx + 1))) {
|
||||
methodSuffix = tracedVar.substring(dotIdx) + methodSuffix;
|
||||
tracedVar = tracedVar.substring(0, dotIdx);
|
||||
}
|
||||
currentParamName = tracedVar;
|
||||
resolvedValue = tracedVar + methodSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> polymorphicEvents = new ArrayList<>();
|
||||
if (!methodSuffix.isEmpty() && methodSuffix.matches("\\.[a-zA-Z0-9_]+\\(\\)")
|
||||
&& currentParamName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
|
||||
String varName = currentParamName;
|
||||
String methodName = methodSuffix.substring(1, methodSuffix.length() - 2);
|
||||
|
||||
// Resolve in the first method in the path where the variable might be declared
|
||||
for (String methodFqn : path) {
|
||||
String declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||
if (declaredType != null) {
|
||||
List<String> typesToInspect = new ArrayList<>();
|
||||
typesToInspect.add(declaredType);
|
||||
typesToInspect.addAll(context.getImplementations(declaredType));
|
||||
|
||||
for (String type : typesToInspect) {
|
||||
Set<String> visited = new HashSet<>();
|
||||
// We must find the compilation unit to pass for simple names!
|
||||
// Let's pass the first available CU for this class
|
||||
TypeDeclaration baseTd = context.getTypeDeclaration(type);
|
||||
CompilationUnit cuToUse = null;
|
||||
if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) {
|
||||
cuToUse = (CompilationUnit) baseTd.getRoot();
|
||||
} else {
|
||||
String entryClassName = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName);
|
||||
if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) {
|
||||
cuToUse = (CompilationUnit) entryTd.getRoot();
|
||||
}
|
||||
}
|
||||
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
|
||||
for (String constant : constants) {
|
||||
if (!polymorphicEvents.contains(constant)) {
|
||||
polymorphicEvents.add(constant);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) {
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.polymorphicEvents(polymorphicEvents)
|
||||
.build();
|
||||
}
|
||||
|
||||
return tp;
|
||||
}
|
||||
|
||||
private int getParameterIndex(String methodFqn, String paramName) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return -1;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
for (int i = 0; i < md.parameters().size(); i++) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
|
||||
if (svd.getName().getIdentifier().equals(paramName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private String getVariableDeclaredType(String methodFqn, String varName) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
for (Object pObj : md.parameters()) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
|
||||
if (svd.getName().getIdentifier().equals(varName)) {
|
||||
return svd.getType().toString();
|
||||
}
|
||||
}
|
||||
final String[] foundType = new String[1];
|
||||
if (md.getBody() != null) {
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement node) {
|
||||
for (Object fragObj : node.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
foundType[0] = node.getType().toString();
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
return foundType[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
||||
if (depth > 20) return Collections.emptyList();
|
||||
String fqn = className + "." + methodName;
|
||||
if (!visited.add(fqn)) return Collections.emptyList();
|
||||
|
||||
List<String> constants = new ArrayList<>();
|
||||
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
|
||||
if (td == null) {
|
||||
}
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
Expression retExpr = node.getExpression();
|
||||
if (retExpr != null) {
|
||||
boolean handled = false;
|
||||
if (retExpr instanceof MethodInvocation mi) {
|
||||
// Follow delegation first
|
||||
String called = resolveCalledMethod(mi);
|
||||
if (called != null && called.contains(".")) {
|
||||
if (visited.contains(called)) {
|
||||
handled = true;
|
||||
} else {
|
||||
String cName = called.substring(0, called.lastIndexOf('.'));
|
||||
String mName = called.substring(called.lastIndexOf('.') + 1);
|
||||
|
||||
TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName);
|
||||
if (targetTd == null) targetTd = context.getTypeDeclaration(cName);
|
||||
|
||||
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||
List<String> delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu);
|
||||
constants.addAll(delegationResult);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!handled) {
|
||||
String val = constantResolver.resolve(retExpr, context);
|
||||
if (val != null) {
|
||||
if (val.startsWith("ENUM_SET:")) {
|
||||
for (String eVal : val.substring(9).split(",")) {
|
||||
constants.add(eVal.substring(eVal.lastIndexOf('.') + 1));
|
||||
}
|
||||
} else {
|
||||
constants.add(val);
|
||||
}
|
||||
} else if (retExpr instanceof QualifiedName qn) {
|
||||
constants.add(qn.toString());
|
||||
} else if (retExpr instanceof SimpleName sn) {
|
||||
constants.add(sn.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
visited.remove(fqn);
|
||||
return constants;
|
||||
}
|
||||
|
||||
private String traceLocalVariable(String methodFqn, String varName) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return null;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
final Expression[] initializer = new Expression[1];
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||
initializer[0] = node.getInitializer();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(Assignment node) {
|
||||
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||
initializer[0] = node.getRightHandSide();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
if (initializer[0] != null) {
|
||||
Expression expr = traceVariable(initializer[0]);
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
// Unwrapper logic: If wrapper method is called, extract its arguments recursively
|
||||
Expression innerMost = unwrapMethodInvocation(mi, 0);
|
||||
if (innerMost instanceof MethodInvocation innerMi) {
|
||||
if (innerMi.getExpression() instanceof SimpleName sn) {
|
||||
return sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()";
|
||||
}
|
||||
return innerMi.getName().getIdentifier() + "()";
|
||||
}
|
||||
if (innerMost instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
return innerMost.toString();
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
return expr.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
|
||||
if (depth > 5) return mi;
|
||||
if (!mi.arguments().isEmpty()) {
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrapMethodInvocation(innerMi, depth + 1);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
return mi;
|
||||
}
|
||||
|
||||
private String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
for (String node : path) {
|
||||
List<CallEdge> edges = callGraph.get(node);
|
||||
if (edges != null) {
|
||||
for (CallEdge edge : edges) {
|
||||
String target = edge.getTargetMethod();
|
||||
if (target != null && (target.contains(".restore") || target.contains(".read"))) {
|
||||
// Persister signatures usually like: restore(stateMachine, contextObj)
|
||||
if (edge.getArguments().size() >= 2) {
|
||||
return edge.getArguments().get(1); // The contextObj / machineId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (MethodDeclaration) parent;
|
||||
}
|
||||
|
||||
private Map<String, List<CallEdge>> buildCallGraph() {
|
||||
graph = new HashMap<>();
|
||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||
cu.accept(new ASTVisitor() {
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
MethodDeclaration md = findEnclosingMethod(node);
|
||||
if (md != null) {
|
||||
TypeDeclaration td = findEnclosingType(md);
|
||||
if (td != null) {
|
||||
String currentMethodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
for (String calledMethod : calledMethods) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
}
|
||||
for (Object argObj : node.arguments()) {
|
||||
if (argObj instanceof ExpressionMethodReference emr) {
|
||||
String typeName = emr.getExpression().toString();
|
||||
if ("this".equals(typeName) || "super".equals(typeName)) {
|
||||
TypeDeclaration td2 = findEnclosingType(node);
|
||||
if (td2 != null) {
|
||||
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
|
||||
if (refMethod != null) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String fallbackTypeFqn = null;
|
||||
ITypeBinding binding = emr.getExpression().resolveTypeBinding();
|
||||
if (binding != null) {
|
||||
fallbackTypeFqn = binding.getQualifiedName();
|
||||
} else if (emr.getExpression() instanceof SimpleName sn) {
|
||||
fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||
}
|
||||
if (fallbackTypeFqn != null) {
|
||||
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SuperMethodInvocation node) {
|
||||
MethodDeclaration md = findEnclosingMethod(node);
|
||||
if (md != null) {
|
||||
TypeDeclaration tdOuter = findEnclosingType(md);
|
||||
if (tdOuter != null) {
|
||||
String currentMethodFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
String calledMethod = null;
|
||||
if (superTd != null) {
|
||||
calledMethod = resolveMethodInType(superTd, methodName);
|
||||
}
|
||||
if (calledMethod == null) {
|
||||
calledMethod = superFqn + "." + methodName;
|
||||
}
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
private List<String> resolveArguments(List<?> astArguments) {
|
||||
List<String> args = new ArrayList<>();
|
||||
for (Object argObj : astArguments) {
|
||||
Expression expr = (Expression) argObj;
|
||||
|
||||
// Extract from lambda
|
||||
if (expr instanceof LambdaExpression le) {
|
||||
ASTNode body = le.getBody();
|
||||
if (body instanceof Expression bodyExpr) {
|
||||
expr = bodyExpr;
|
||||
} else if (body instanceof Block block) {
|
||||
for (Object stmtObj : block.statements()) {
|
||||
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
||||
expr = rs.getExpression();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expr instanceof ExpressionMethodReference emr) {
|
||||
TypeDeclaration td = findEnclosingType(expr);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
for (Object stmtObj : md.getBody().statements()) {
|
||||
if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) {
|
||||
expr = rs.getExpression();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract from constructor args (e.g., new CustomMessage(OrderEvent.PROCESS, ...))
|
||||
Expression tracedExpr = traceVariable(expr);
|
||||
if (tracedExpr instanceof QualifiedName || tracedExpr instanceof ClassInstanceCreation || tracedExpr instanceof StringLiteral || tracedExpr instanceof NumberLiteral) {
|
||||
expr = tracedExpr; // Only accept trace if it resolves to a pure constant/primitive/constructor
|
||||
}
|
||||
|
||||
if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) {
|
||||
Expression firstArg = (Expression) cic.arguments().get(0);
|
||||
String resolved = constantResolver.resolve(firstArg, context);
|
||||
if (resolved != null) {
|
||||
expr = firstArg; // Only unwrap if it's actually a constant
|
||||
}
|
||||
}
|
||||
|
||||
String val;
|
||||
if (expr instanceof MethodInvocation mi && mi.getExpression() instanceof SimpleName
|
||||
&& mi.arguments().isEmpty()) {
|
||||
// Keep local getter chains (e.g. event.getType()) for downstream parameter tracing.
|
||||
val = expr.toString();
|
||||
} else {
|
||||
val = constantResolver.resolve(expr, context);
|
||||
if (val == null) {
|
||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
||||
}
|
||||
}
|
||||
args.add(val);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||
String baseCalled = resolveCalledMethod(node);
|
||||
if (baseCalled == null) return Collections.emptyList();
|
||||
|
||||
List<String> allResolved = new ArrayList<>();
|
||||
allResolved.add(baseCalled);
|
||||
|
||||
if (baseCalled.contains(".")) {
|
||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||
|
||||
List<String> impls = context.getImplementations(className);
|
||||
for (String impl : impls) {
|
||||
allResolved.add(impl + "." + methodName);
|
||||
}
|
||||
}
|
||||
|
||||
return allResolved;
|
||||
}
|
||||
|
||||
private String resolveCalledMethod(MethodInvocation node) {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return resolveMethodInType(td, methodName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||
if (binding != null) {
|
||||
return binding.getQualifiedName() + "." + methodName;
|
||||
}
|
||||
|
||||
if (receiver instanceof ThisExpression) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return context.getFqn(td) + "." + methodName;
|
||||
}
|
||||
}
|
||||
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
String fallbackTypeFqn = resolveReceiverTypeFallback(sn);
|
||||
if (fallbackTypeFqn != null) {
|
||||
return fallbackTypeFqn + "." + methodName;
|
||||
}
|
||||
String receiverName = sn.getIdentifier();
|
||||
return receiverName + "." + methodName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
||||
String varName = receiverNameNode.getIdentifier();
|
||||
|
||||
// 1. Check local variables in enclosing method
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||
if (enclosingMethod != null) {
|
||||
// Check parameters
|
||||
for (Object paramObj : enclosingMethod.parameters()) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||
if (svd.getName().getIdentifier().equals(varName)) {
|
||||
return resolveTypeToFqn(svd.getType(), receiverNameNode);
|
||||
}
|
||||
}
|
||||
// Check method body (local variables)
|
||||
if (enclosingMethod.getBody() != null) {
|
||||
Type[] foundType = new Type[1];
|
||||
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement node) {
|
||||
for (Object fragObj : node.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
foundType[0] = node.getType();
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (foundType[0] != null) {
|
||||
return resolveTypeToFqn(foundType[0], receiverNameNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check fields in enclosing class
|
||||
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||
if (enclosingType != null) {
|
||||
for (FieldDeclaration field : enclosingType.getFields()) {
|
||||
for (Object fragObj : field.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
return resolveTypeToFqn(field.getType(), receiverNameNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
||||
if (type == null) return null;
|
||||
String simpleName;
|
||||
if (type.isSimpleType()) {
|
||||
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
|
||||
} else {
|
||||
simpleName = type.toString();
|
||||
}
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
|
||||
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
|
||||
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
private String resolveMethodInType(TypeDeclaration td, String methodName) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
TypeDeclaration declaringTd = findEnclosingType(md);
|
||||
return context.getFqn(declaringTd) + "." + methodName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||
if (!visited.add(start)) return null; // Path-scoped cycle detection
|
||||
|
||||
List<CallEdge> neighbors = graph.get(start);
|
||||
if (neighbors != null) {
|
||||
for (CallEdge edge : neighbors) {
|
||||
String neighbor = edge.getTargetMethod();
|
||||
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
||||
visited.remove(start);
|
||||
return new ArrayList<>(List.of(start, target));
|
||||
}
|
||||
List<String> path = findPath(neighbor, target, graph, visited);
|
||||
if (path != null) {
|
||||
path.add(0, start);
|
||||
visited.remove(start);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Path search dead-end at {} when looking for {}", start, target);
|
||||
}
|
||||
|
||||
visited.remove(start);
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||
if (target.endsWith("." + neighbor)) return true;
|
||||
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
|
||||
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
||||
return simpleNeighbor.equals(simpleTarget);
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof TypeDeclaration)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return (TypeDeclaration) parent;
|
||||
}
|
||||
private Expression traceVariable(Expression expr) {
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String varName = sn.getIdentifier();
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
||||
if (enclosingMethod != null) {
|
||||
final Expression[] initializer = new Expression[1];
|
||||
enclosingMethod.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||
initializer[0] = node.getInitializer();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(Assignment node) {
|
||||
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
||||
initializer[0] = node.getRightHandSide();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (initializer[0] != null) {
|
||||
return traceVariable(initializer[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ public class CallGraphPathFinder {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void clearAnalysisCaches() {
|
||||
heuristicCache.clear();
|
||||
memoCache.clear();
|
||||
unreachableCache.clear();
|
||||
}
|
||||
|
||||
public List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||
if (!visited.add(start)) {
|
||||
@@ -360,19 +366,10 @@ public class CallGraphPathFinder {
|
||||
|
||||
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
|
||||
|
||||
List<String> impls = context.getImplementations(classTarget);
|
||||
if (impls != null) {
|
||||
for (String impl : impls) {
|
||||
if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true;
|
||||
}
|
||||
if (context.areClassesPolymorphicallyCompatible(classNeighbor, classTarget)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,9 @@ public class ConstantExtractor {
|
||||
} else if (expr instanceof MethodInvocation mi) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
|
||||
log.trace("Processing mi: {}", mi);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Processing mi: {}", mi);
|
||||
}
|
||||
if (("get".equals(methodName) || "getOrDefault".equals(methodName)) && mi.getExpression() != null) {
|
||||
if (isKeyedLookupMethod(mi)) {
|
||||
extractMapReceiverConstants(mi.getExpression(), constants);
|
||||
@@ -294,19 +296,37 @@ public class ConstantExtractor {
|
||||
Set<String> visited,
|
||||
CompilationUnit contextCu,
|
||||
ResolutionBudget budget) {
|
||||
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
|
||||
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
||||
return resolveMethodReturnConstant(className, methodName, depth, visited, contextCu, budget, true);
|
||||
}
|
||||
|
||||
public List<String> resolveMethodReturnConstant(
|
||||
String className,
|
||||
String methodName,
|
||||
int depth,
|
||||
Set<String> visited,
|
||||
CompilationUnit contextCu,
|
||||
ResolutionBudget budget,
|
||||
boolean allowWidenToImplementations) {
|
||||
final boolean debug = log.isDebugEnabled();
|
||||
if (debug) {
|
||||
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
|
||||
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
||||
}
|
||||
if (budget == null) {
|
||||
budget = ResolutionBudget.defaults();
|
||||
}
|
||||
final ResolutionBudget activeBudget = budget;
|
||||
if (activeBudget.isMethodReturnExhausted(depth)) {
|
||||
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
|
||||
if (debug) {
|
||||
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String fqn = className + "." + methodName;
|
||||
if (!visited.add(fqn)) {
|
||||
log.debug("Exiting resolveMethodReturnConstant early (cycle detected) for fqn={}", fqn);
|
||||
if (debug) {
|
||||
log.debug("Exiting resolveMethodReturnConstant early (cycle detected) for fqn={}", fqn);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -316,28 +336,48 @@ public class ConstantExtractor {
|
||||
tempTd = context.getTypeDeclaration(className);
|
||||
}
|
||||
final TypeDeclaration td = tempTd;
|
||||
boolean widenToImplementations = td == null
|
||||
boolean widenToImplementations = allowWidenToImplementations
|
||||
&& (td == null
|
||||
|| td.isInterface()
|
||||
|| Modifier.isAbstract(td.getModifiers());
|
||||
|| Modifier.isAbstract(td.getModifiers()));
|
||||
|
||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter() && !widenToImplementations) {
|
||||
if (indexedAccessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
|
||||
List<String> constantGetterValues = extractConstantGetterReturn(
|
||||
indexedAccessor.get().ownerFqn(), methodName, contextCu, visited);
|
||||
if (!constantGetterValues.isEmpty()) {
|
||||
visited.remove(fqn);
|
||||
return constantGetterValues;
|
||||
}
|
||||
}
|
||||
List<String> indexedConstants = resolveIndexedGetterFieldConstants(
|
||||
indexedAccessor.get(), contextCu, visited);
|
||||
if (!indexedConstants.isEmpty()) {
|
||||
if (widenToImplementations && depth == 0) {
|
||||
Optional<List<String>> precomputed = context.getPolymorphicAccessorIndex()
|
||||
.widenedReturnConstants(className, methodName);
|
||||
if (precomputed.isPresent()) {
|
||||
visited.remove(fqn);
|
||||
return indexedConstants;
|
||||
return new ArrayList<>(precomputed.get());
|
||||
}
|
||||
}
|
||||
|
||||
List<String> constants = new ArrayList<>();
|
||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
||||
boolean indexedOnRequestedType = className.equals(indexedAccessor.get().ownerFqn());
|
||||
if (!widenToImplementations || indexedOnRequestedType) {
|
||||
if (indexedAccessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
|
||||
List<String> constantGetterValues = extractConstantGetterReturn(
|
||||
indexedAccessor.get().ownerFqn(), methodName, contextCu, visited);
|
||||
if (!constantGetterValues.isEmpty()) {
|
||||
if (!widenToImplementations) {
|
||||
visited.remove(fqn);
|
||||
return constantGetterValues;
|
||||
}
|
||||
constants.addAll(constantGetterValues);
|
||||
}
|
||||
}
|
||||
if (constants.isEmpty()) {
|
||||
List<String> indexedConstants = resolveIndexedGetterFieldConstants(
|
||||
indexedAccessor.get(), className, contextCu, visited);
|
||||
if (!indexedConstants.isEmpty()) {
|
||||
if (!widenToImplementations) {
|
||||
visited.remove(fqn);
|
||||
return indexedConstants;
|
||||
}
|
||||
constants.addAll(indexedConstants);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
@@ -361,7 +401,7 @@ public class ConstantExtractor {
|
||||
|
||||
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||
List<String> delegationResult = resolveMethodReturnConstant(
|
||||
cName, mName, depth + 1, visited, contextCu, activeBudget);
|
||||
cName, mName, depth + 1, visited, contextCu, activeBudget, allowWidenToImplementations);
|
||||
constants.addAll(delegationResult);
|
||||
handled = true;
|
||||
}
|
||||
@@ -381,7 +421,7 @@ public class ConstantExtractor {
|
||||
|
||||
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||
List<String> delegationResult = resolveMethodReturnConstant(
|
||||
cName, mName, depth + 1, visited, contextCu, activeBudget);
|
||||
cName, mName, depth + 1, visited, contextCu, activeBudget, allowWidenToImplementations);
|
||||
constants.addAll(delegationResult);
|
||||
handled = true;
|
||||
}
|
||||
@@ -423,29 +463,56 @@ public class ConstantExtractor {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (constants.isEmpty() && widenToImplementations) {
|
||||
if (widenToImplementations) {
|
||||
List<String> impls = context.getImplementations(className);
|
||||
if (impls != null) {
|
||||
for (String implName : impls) {
|
||||
List<String> delegationResult = resolveMethodReturnConstant(
|
||||
implName, methodName, depth + 1, visited, contextCu, activeBudget);
|
||||
constants.addAll(delegationResult);
|
||||
implName, methodName, depth + 1, visited, contextCu, activeBudget, allowWidenToImplementations);
|
||||
for (String value : delegationResult) {
|
||||
if (!constants.contains(value)) {
|
||||
constants.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (widenToImplementations) {
|
||||
List<String> impls = context.getImplementations(className);
|
||||
if (impls != null) {
|
||||
for (String implName : impls) {
|
||||
List<String> delegationResult = resolveMethodReturnConstant(
|
||||
implName, methodName, depth + 1, visited, contextCu, activeBudget);
|
||||
for (String value : delegationResult) {
|
||||
if (!constants.contains(value)) {
|
||||
constants.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visited.remove(fqn);
|
||||
log.debug("resolveMethodReturnConstant className={} methodName={} returns: {}", className, methodName, constants);
|
||||
log.debug("resolveMethodReturnConstant for class={}, method={} resolved constants: {}", className, methodName, constants);
|
||||
if (debug) {
|
||||
log.debug("resolveMethodReturnConstant className={} methodName={} returns: {}", className, methodName, constants);
|
||||
}
|
||||
return constants;
|
||||
}
|
||||
|
||||
public List<String> resolveIndexedGetterFieldConstants(
|
||||
AccessorSummary accessor,
|
||||
String requestingTypeFqn,
|
||||
CompilationUnit contextCu,
|
||||
Set<String> visited) {
|
||||
if (accessor != null && isSafeForPrecomputedFieldConstants(accessor)) {
|
||||
Optional<List<String>> precomputed = context.getIndexedFieldConstantsIndex()
|
||||
.lookup(accessor.ownerFqn(), accessor.methodName());
|
||||
if (precomputed.isPresent()) {
|
||||
return new ArrayList<>(precomputed.get());
|
||||
}
|
||||
}
|
||||
return AccessorFieldResolver.resolveFieldConstants(
|
||||
accessor,
|
||||
requestingTypeFqn,
|
||||
context,
|
||||
constructorAnalyzer,
|
||||
contextCu,
|
||||
@@ -479,9 +546,14 @@ public class ConstantExtractor {
|
||||
}
|
||||
}
|
||||
List<String> implementations = context.getImplementations(className);
|
||||
if (implementations == null) {
|
||||
if (implementations == null || implementations.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<AccessorSummary> precomputed = context.getPolymorphicAccessorIndex()
|
||||
.representativeImplAccessor(className, methodName);
|
||||
if (precomputed.isPresent()) {
|
||||
return precomputed;
|
||||
}
|
||||
for (String implementation : implementations) {
|
||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
||||
if (implAccessor.isPresent()) {
|
||||
@@ -522,6 +594,20 @@ public class ConstantExtractor {
|
||||
return constants;
|
||||
}
|
||||
|
||||
|
||||
private boolean isSafeForPrecomputedFieldConstants(AccessorSummary accessor) {
|
||||
if (accessor.fieldName() == null || accessor.declaringFqn() == null) {
|
||||
return accessor.kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER;
|
||||
}
|
||||
if (!accessor.ownerFqn().equals(accessor.declaringFqn())) {
|
||||
return false;
|
||||
}
|
||||
TypeDeclaration ownerType = context.getTypeDeclaration(accessor.ownerFqn());
|
||||
return ownerType != null
|
||||
&& !ownerType.isInterface()
|
||||
&& !Modifier.isAbstract(ownerType.getModifiers());
|
||||
}
|
||||
|
||||
public String parseEnumSetElement(String eVal) {
|
||||
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ public class ConstructorAnalyzer {
|
||||
private final ConstantResolver constantResolver;
|
||||
private VariableTracer variableTracer;
|
||||
private ConstantExtractor constantExtractor;
|
||||
private final Map<String, ClassInstanceCreation> returnedCicCache = new HashMap<>();
|
||||
private final Set<String> returnedCicAbsent = new HashSet<>();
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RhsResolver {
|
||||
@@ -33,6 +35,11 @@ public class ConstructorAnalyzer {
|
||||
this.constantExtractor = constantExtractor;
|
||||
}
|
||||
|
||||
public void clearReturnedCicCache() {
|
||||
returnedCicCache.clear();
|
||||
returnedCicAbsent.clear();
|
||||
}
|
||||
|
||||
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||
return traceFieldInConstructors(td, fieldName, context, visited, ResolutionBudget.defaults());
|
||||
}
|
||||
@@ -480,7 +487,20 @@ public class ConstructorAnalyzer {
|
||||
|
||||
public ClassInstanceCreation findReturnedCIC(
|
||||
String className, String methodName, CodebaseContext context) {
|
||||
return findReturnedCIC(className, methodName, context, new HashSet<>(), 0);
|
||||
String cacheKey = className + "#" + methodName;
|
||||
if (returnedCicAbsent.contains(cacheKey)) {
|
||||
return null;
|
||||
}
|
||||
if (returnedCicCache.containsKey(cacheKey)) {
|
||||
return returnedCicCache.get(cacheKey);
|
||||
}
|
||||
ClassInstanceCreation resolved = findReturnedCIC(className, methodName, context, new HashSet<>(), 0);
|
||||
if (resolved != null) {
|
||||
returnedCicCache.put(cacheKey, resolved);
|
||||
} else {
|
||||
returnedCicAbsent.add(cacheKey);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private ClassInstanceCreation findReturnedCIC(
|
||||
@@ -575,11 +595,15 @@ public class ConstructorAnalyzer {
|
||||
|
||||
try {
|
||||
Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor);
|
||||
log.debug("RESOLVE_GETTER: td={}, fieldValues={}", context.getFqn(td), fieldValues);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("RESOLVE_GETTER: td={}, fieldValues={}", context.getFqn(td), fieldValues);
|
||||
}
|
||||
if (fieldValues.isEmpty()) return Collections.emptyList();
|
||||
|
||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
||||
log.debug("RESOLVE_GETTER: result={}", result);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("RESOLVE_GETTER: result={}", result);
|
||||
}
|
||||
return result != null ? List.of(result) : Collections.emptyList();
|
||||
} finally {
|
||||
visited.remove(getterKey);
|
||||
|
||||
@@ -136,7 +136,11 @@ public final class ExpressionAccessClassifier {
|
||||
return "Map";
|
||||
}
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
return lookupDeclaredType(scopeMethod, sn.getIdentifier());
|
||||
String declared = lookupDeclaredType(scopeMethod, sn.getIdentifier());
|
||||
if (declared != null) {
|
||||
return declared;
|
||||
}
|
||||
return lookupFieldType(classNameFromScopeMethod(scopeMethod), sn.getIdentifier());
|
||||
}
|
||||
if (receiver instanceof ThisExpression) {
|
||||
return classNameFromScopeMethod(scopeMethod);
|
||||
|
||||
@@ -2,6 +2,7 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -107,6 +108,10 @@ public class GenericEventDetector {
|
||||
}
|
||||
|
||||
private String extractEventFromReceiver(Expression receiver) {
|
||||
String reactivePayload = ReactiveExpressionSupport.extractPayload(receiver, constantResolver, context);
|
||||
if (reactivePayload != null) {
|
||||
return reactivePayload;
|
||||
}
|
||||
if (receiver == null) return null;
|
||||
if (receiver instanceof MethodInvocation mi) {
|
||||
String mName = mi.getName().getIdentifier();
|
||||
@@ -233,6 +238,11 @@ public class GenericEventDetector {
|
||||
} else {
|
||||
eventValue = context.resolveExpression(eventExpr);
|
||||
}
|
||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||
eventExpr, constantResolver, context);
|
||||
if (remapped != null) {
|
||||
eventValue = remapped;
|
||||
}
|
||||
}
|
||||
|
||||
MethodDeclaration method = findEnclosingMethod(node);
|
||||
|
||||
@@ -141,32 +141,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return graph;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) {
|
||||
// Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
|
||||
// where we can substitute the actual receiver from the call edge
|
||||
boolean isThisOrSuperGetter = (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi
|
||||
&& (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression || mi.getExpression() instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation)
|
||||
&& mi.arguments().isEmpty()) || (originalExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi && smi.arguments().isEmpty());
|
||||
|
||||
if (isThisOrSuperGetter) {
|
||||
return originalExpr.toString(); // Preserve "this.getTransitionType()" or "super.getEvent()" for later substitution
|
||||
}
|
||||
|
||||
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized
|
||||
// from a ClassInstanceCreation (directly or via a factory method). This avoids
|
||||
// the broad ENUM_SET fallback when the getter transforms one enum to another.
|
||||
if (originalExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation getterMi
|
||||
&& getterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName instanceSn
|
||||
&& getterMi.arguments().isEmpty()) {
|
||||
String narrowed = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
|
||||
if (narrowed != null) {
|
||||
return narrowed;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedValue;
|
||||
}
|
||||
private String findDefiningClass(String className, String methodName) {
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) return null;
|
||||
@@ -230,351 +204,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
||||
String varName = receiverNameNode.getIdentifier();
|
||||
|
||||
CompilationUnit cu = receiverNameNode.getRoot() instanceof CompilationUnit ? (CompilationUnit) receiverNameNode.getRoot() : null;
|
||||
TypeDeclaration staticTd = context.getTypeDeclaration(varName, cu);
|
||||
if (staticTd != null) {
|
||||
return context.getFqn(staticTd);
|
||||
}
|
||||
TypeDeclaration fallbackStaticTd = context.getTypeDeclaration(varName);
|
||||
if (fallbackStaticTd != null) {
|
||||
return context.getFqn(fallbackStaticTd);
|
||||
}
|
||||
|
||||
// 1. Check local variables in enclosing method
|
||||
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||
if (enclosingMethod != null) {
|
||||
// Check parameters
|
||||
for (Object paramObj : enclosingMethod.parameters()) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||
if (svd.getName().getIdentifier().equals(varName)) {
|
||||
return resolveTypeToFqn(svd.getType(), receiverNameNode);
|
||||
}
|
||||
}
|
||||
// Check method body (local variables)
|
||||
if (enclosingMethod.getBody() != null) {
|
||||
Type[] foundType = new Type[1];
|
||||
enclosingMethod.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement node) {
|
||||
for (Object fragObj : node.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
foundType[0] = node.getType();
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (foundType[0] != null) {
|
||||
return resolveTypeToFqn(foundType[0], receiverNameNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check fields in enclosing class and its superclasses
|
||||
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||
TypeDeclaration currentType = enclosingType;
|
||||
while (currentType != null) {
|
||||
for (FieldDeclaration field : currentType.getFields()) {
|
||||
for (Object fragObj : field.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
return resolveTypeToFqn(field.getType(), receiverNameNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
String superclass = context.getSuperclassFqn(currentType);
|
||||
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String resolveTypeToFqn(Type type, ASTNode contextNode) {
|
||||
if (type == null) return null;
|
||||
String simpleName;
|
||||
if (type.isSimpleType()) {
|
||||
simpleName = ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isParameterizedType()) {
|
||||
return resolveTypeToFqn(((ParameterizedType) type).getType(), contextNode);
|
||||
} else {
|
||||
simpleName = type.toString();
|
||||
}
|
||||
|
||||
CompilationUnit cu = (CompilationUnit) contextNode.getRoot();
|
||||
TypeDeclaration td = context.getTypeDeclaration(simpleName, cu);
|
||||
if (td != null) {
|
||||
return context.getFqn(td);
|
||||
}
|
||||
|
||||
// Fallback to import matching if CodebaseContext doesn't know it (e.g., external library)
|
||||
for (Object impObj : cu.imports()) {
|
||||
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||
String impName = imp.getName().getFullyQualifiedName();
|
||||
if (impName.endsWith("." + simpleName)) {
|
||||
return impName;
|
||||
}
|
||||
}
|
||||
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to narrow the resolution of {@code instanceVar.getterMethod()} when the
|
||||
* instance variable is initialised from a {@link org.eclipse.jdt.core.dom.ClassInstanceCreation}
|
||||
* (either directly or via a factory/mapper method).
|
||||
*
|
||||
* <p>This handles both:
|
||||
* <ul>
|
||||
* <li>Simple pass-through: {@code Wrapper(MyEnum e){this.e=e;} getE(){return e;}}</li>
|
||||
* <li>Transformation: {@code getSmEvent(){return switch(dtoEnum){case A->SmEvents.X;…};}}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the resolved constant string, or {@code null} when narrowing is not possible
|
||||
*/
|
||||
private String tryNarrowGetterViaLocalVarChain(
|
||||
MethodInvocation getterCall, SimpleName instanceSn) {
|
||||
|
||||
MethodDeclaration enclosing = findEnclosingMethod(getterCall);
|
||||
if (enclosing == null || enclosing.getBody() == null) return null;
|
||||
|
||||
String varName = instanceSn.getIdentifier();
|
||||
String getterName = getterCall.getName().getIdentifier();
|
||||
|
||||
// Find the declared type and initialiser of the local variable
|
||||
String[] varTypeName = {null};
|
||||
Expression[] initExpr = {null};
|
||||
enclosing.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationStatement vds) {
|
||||
for (Object fragObj : vds.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
varTypeName[0] = vds.getType().toString();
|
||||
initExpr[0] = frag.getInitializer();
|
||||
}
|
||||
}
|
||||
return super.visit(vds);
|
||||
}
|
||||
});
|
||||
if (varTypeName[0] == null) return null;
|
||||
|
||||
// Obtain the ClassInstanceCreation that created the object
|
||||
org.eclipse.jdt.core.dom.ClassInstanceCreation cic = null;
|
||||
String receiverType = null;
|
||||
if (initExpr[0] instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation directCic) {
|
||||
cic = directCic;
|
||||
} else if (initExpr[0] instanceof MethodInvocation factoryMi
|
||||
&& (factoryMi.getExpression() instanceof SimpleName || factoryMi.getExpression() instanceof QualifiedName)) {
|
||||
// e.g. "mapper.toMsg(dto)" or "com.example.Factory.toMsg()"
|
||||
if (factoryMi.getExpression() instanceof SimpleName receiverSn) {
|
||||
receiverType = resolveReceiverTypeFallback(receiverSn);
|
||||
} else if (factoryMi.getExpression() instanceof QualifiedName qn) {
|
||||
receiverType = qn.getFullyQualifiedName();
|
||||
}
|
||||
if (receiverType != null) {
|
||||
cic = findReturnedCIC(receiverType, factoryMi.getName().getIdentifier(), context);
|
||||
}
|
||||
}
|
||||
if (cic == null) {
|
||||
// Support builder pattern: e.g. EventWrapper.builder().event(TransitionEnum.STATE_X).build()
|
||||
if (initExpr[0] instanceof MethodInvocation initMi) {
|
||||
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||
Expression currentMi = initMi;
|
||||
while (currentMi instanceof MethodInvocation chainMi) {
|
||||
String mName = chainMi.getName().getIdentifier();
|
||||
if (mName.equalsIgnoreCase("set" + propName) || mName.equalsIgnoreCase(propName)) {
|
||||
if (!chainMi.arguments().isEmpty()) {
|
||||
return chainMi.arguments().get(0).toString();
|
||||
}
|
||||
}
|
||||
currentMi = chainMi.getExpression();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Resolve the variable's declared type to a TypeDeclaration
|
||||
CompilationUnit cu = (CompilationUnit) getterCall.getRoot();
|
||||
TypeDeclaration varTypeTd = context.getTypeDeclaration(varTypeName[0], cu);
|
||||
if (varTypeTd == null) varTypeTd = context.getTypeDeclaration(varTypeName[0]);
|
||||
if (varTypeTd == null) return null;
|
||||
|
||||
// Evaluate the getter body with field values substituted from the CIC's constructor args
|
||||
java.util.Map<String, String> initialLocals = new java.util.HashMap<>();
|
||||
if (initExpr[0] instanceof MethodInvocation factoryMi) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(context.getTypeDeclaration(receiverType != null ? receiverType : varTypeName[0]), factoryMi.getName().getIdentifier(), true);
|
||||
if (md != null) {
|
||||
for (int i = 0; i < md.parameters().size() && i < factoryMi.arguments().size(); i++) {
|
||||
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
|
||||
Expression arg = (Expression) factoryMi.arguments().get(i);
|
||||
String resolvedVal = constantResolver.resolve(arg, context);
|
||||
if (resolvedVal != null) {
|
||||
initialLocals.put(param.getName().getIdentifier(), resolvedVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
java.util.Map<String, String> fieldValues = constructorAnalyzer.buildFieldValuesFromCIC(varTypeTd, cic, context, this::evaluateMethodCallInConstructor, initialLocals);
|
||||
if (fieldValues.isEmpty()) return null;
|
||||
|
||||
// Track setter calls on the variable between its initialization and the getter call
|
||||
// to capture field updates like e.setType("NEW_VALUE")
|
||||
trackSetterCallsBetween(enclosing.getBody(), varName, getterCall, fieldValues, context);
|
||||
|
||||
MethodDeclaration getter = context.findMethodDeclaration(varTypeTd, getterName, true);
|
||||
if (getter == null || getter.getBody() == null) return null;
|
||||
|
||||
return constantResolver.evaluateMethodBodyWithLocals(
|
||||
getter, fieldValues, context, new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the method body for setter calls or field assignments to the given variable
|
||||
* that occur between the variable's initialization and the getter call.
|
||||
* Updates fieldValues with any field values set via setters.
|
||||
*/
|
||||
private void trackSetterCallsBetween(org.eclipse.jdt.core.dom.Block methodBody, String varName,
|
||||
org.eclipse.jdt.core.dom.MethodInvocation getterCall,
|
||||
java.util.Map<String, String> fieldValues,
|
||||
CodebaseContext context) {
|
||||
java.util.List<org.eclipse.jdt.core.dom.ASTNode> statements = new java.util.ArrayList<>();
|
||||
for (Object stmtObj : methodBody.statements()) {
|
||||
statements.add((org.eclipse.jdt.core.dom.ASTNode) stmtObj);
|
||||
}
|
||||
|
||||
int varDeclIndex = -1;
|
||||
int getterCallIndex = -1;
|
||||
for (int i = 0; i < statements.size(); i++) {
|
||||
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
|
||||
// Find variable declaration
|
||||
if (varDeclIndex == -1) {
|
||||
if (stmt instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement vds) {
|
||||
for (Object fragObj : vds.fragments()) {
|
||||
if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment frag
|
||||
&& frag.getName().getIdentifier().equals(varName)) {
|
||||
varDeclIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Find getter call
|
||||
if (getterCallIndex == -1) {
|
||||
if (stmt.toString().contains(getterCall.toString())) {
|
||||
getterCallIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (varDeclIndex >= 0 && getterCallIndex > varDeclIndex) {
|
||||
// Scan statements between var declaration and getter call
|
||||
for (int i = varDeclIndex + 1; i < getterCallIndex; i++) {
|
||||
org.eclipse.jdt.core.dom.ASTNode stmt = statements.get(i);
|
||||
if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) {
|
||||
org.eclipse.jdt.core.dom.Expression expr = es.getExpression();
|
||||
// Check for setter calls: var.setField(value)
|
||||
if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
|
||||
if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
|
||||
&& sn.getIdentifier().equals(varName)) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
// Check if it's a setter (setXxx or xxx)
|
||||
if (methodName.startsWith("set") && methodName.length() > 3) {
|
||||
String fieldName = Character.toLowerCase(methodName.charAt(3))
|
||||
+ methodName.substring(4);
|
||||
if (!mi.arguments().isEmpty()) {
|
||||
org.eclipse.jdt.core.dom.Expression arg = (org.eclipse.jdt.core.dom.Expression) mi.arguments().get(0);
|
||||
String val = constantResolver.resolve(arg, context);
|
||||
if (val != null) {
|
||||
fieldValues.put(fieldName, val);
|
||||
fieldValues.put("this." + fieldName, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for direct field assignments: var.field = value
|
||||
else if (expr instanceof org.eclipse.jdt.core.dom.Assignment assignment) {
|
||||
if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) {
|
||||
if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn
|
||||
&& sn.getIdentifier().equals(varName)) {
|
||||
String fieldName = fa.getName().getIdentifier();
|
||||
org.eclipse.jdt.core.dom.Expression rhs = assignment.getRightHandSide();
|
||||
String val = constantResolver.resolve(rhs, context);
|
||||
if (val != null) {
|
||||
fieldValues.put(fieldName, val);
|
||||
fieldValues.put("this." + fieldName, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a string representation of the receiver expression from a MethodInvocation.
|
||||
* Handles ThisExpression, SimpleName, and other expression types.
|
||||
*/
|
||||
protected String getReceiverString(Expression receiver) {
|
||||
if (receiver == null) {
|
||||
return null;
|
||||
}
|
||||
if (receiver instanceof ThisExpression) {
|
||||
return "this";
|
||||
}
|
||||
if (receiver instanceof SuperMethodInvocation) {
|
||||
return "super";
|
||||
}
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
return receiver.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the receiver expression used in the caller method when calling the target method.
|
||||
* Walks the caller's AST to find a MethodInvocation with the target method name
|
||||
* and returns the receiver string.
|
||||
*/
|
||||
protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
|
||||
int lastDot = callerFqn.lastIndexOf('.');
|
||||
if (lastDot < 0) return null;
|
||||
String callerClass = callerFqn.substring(0, lastDot);
|
||||
String callerMethod = callerFqn.substring(lastDot + 1);
|
||||
|
||||
int targetLastDot = targetMethodFqn.lastIndexOf('.');
|
||||
if (targetLastDot < 0) return null;
|
||||
String targetMethodName = targetMethodFqn.substring(targetLastDot + 1);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
|
||||
if (callerTd == null) return null;
|
||||
|
||||
MethodDeclaration callerMd = context.findMethodDeclaration(callerTd, callerMethod, false);
|
||||
if (callerMd == null || callerMd.getBody() == null) return null;
|
||||
|
||||
final String[] foundReceiver = {null};
|
||||
callerMd.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if (foundReceiver[0] != null) return false;
|
||||
String called = resolveCalledMethod(node);
|
||||
if (called != null && (called.equals(targetMethodFqn) || called.endsWith("." + targetMethodName))) {
|
||||
foundReceiver[0] = getReceiverString(node.getExpression());
|
||||
return false;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return foundReceiver[0];
|
||||
}
|
||||
|
||||
protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
|
||||
Expression receiver = mi.getExpression();
|
||||
String receiverType = null;
|
||||
|
||||
@@ -46,7 +46,8 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
for (String calledMethod : calledMethods) {
|
||||
CallEdge edge = new CallEdge(calledMethod, args);
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(resolveConstraint(node, calledMethod, constraint));
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
@@ -64,7 +65,8 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
CallEdge edge = new CallEdge(refMethod, implicitArgs);
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(refMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
@@ -85,13 +87,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs);
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
CallEdge implEdge = new CallEdge(impl + "." + emr.getName().getIdentifier(), args);
|
||||
CallEdge implEdge = new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver);
|
||||
implEdge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(implEdge);
|
||||
}
|
||||
@@ -125,8 +128,9 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
calledMethod = superFqn + "." + methodName;
|
||||
}
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
String receiver = "super";
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||
CallEdge edge = new CallEdge(calledMethod, args);
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
@@ -196,16 +200,24 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
nameBinding = fa.resolveFieldBinding();
|
||||
}
|
||||
if (nameBinding instanceof IVariableBinding varBinding) {
|
||||
log.debug("CALLGRAPH RESOLVING BEAN FOR BINDING: {} calling {}", varBinding.getName(), methodName);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("CALLGRAPH RESOLVING BEAN FOR BINDING: {} calling {}", varBinding.getName(), methodName);
|
||||
}
|
||||
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||
if (concreteFqn != null) {
|
||||
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
|
||||
}
|
||||
return concreteFqn + "." + methodName;
|
||||
} else {
|
||||
log.debug(" -> RESOLVE FAILED, RETURNED NULL");
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(" -> RESOLVE FAILED, RETURNED NULL");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("CALLGRAPH NAME BINDING IS NOT VARIABLE: {} calling {}", nameBinding, methodName);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("CALLGRAPH NAME BINDING IS NOT VARIABLE: {} calling {}", nameBinding, methodName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,88 +272,6 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
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 and its superclasses
|
||||
TypeDeclaration enclosingType = findEnclosingType(receiverNameNode);
|
||||
TypeDeclaration currentType = enclosingType;
|
||||
while (currentType != null) {
|
||||
for (FieldDeclaration field : currentType.getFields()) {
|
||||
for (Object fragObj : field.fragments()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName)) {
|
||||
return resolveTypeToFqn(field.getType(), receiverNameNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
String superclass = context.getSuperclassFqn(currentType);
|
||||
currentType = superclass != null ? context.getTypeDeclaration(superclass) : null;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -31,7 +31,6 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
private final SpringComponentDetector componentDetector;
|
||||
private List<TriggerPoint> cachedTriggers;
|
||||
private List<EntryPoint> cachedEntryPoints;
|
||||
private List<CallChain> cachedCallChains;
|
||||
|
||||
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||
this.context = context;
|
||||
@@ -94,17 +93,15 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
|
||||
@Override
|
||||
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
if (cachedCallChains != null) {
|
||||
return cachedCallChains;
|
||||
}
|
||||
List<EntryPoint> allEntryPoints = findEntryPoints();
|
||||
List<TriggerPoint> allTriggers = triggers != null && !triggers.isEmpty()
|
||||
List<EntryPoint> scopedEntryPoints = entryPoints != null && !entryPoints.isEmpty()
|
||||
? entryPoints
|
||||
: findEntryPoints();
|
||||
List<TriggerPoint> scopedTriggers = triggers != null && !triggers.isEmpty()
|
||||
? triggers
|
||||
: findTriggerPoints();
|
||||
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers",
|
||||
allEntryPoints.size(), allTriggers.size());
|
||||
cachedCallChains = callGraphEngine.findChains(allEntryPoints, allTriggers);
|
||||
return cachedCallChains;
|
||||
scopedEntryPoints.size(), scopedTriggers.size());
|
||||
return callGraphEngine.findChains(scopedEntryPoints, scopedTriggers);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* Walks a call path forward, propagating known parameter / local bindings so switch-case
|
||||
@@ -20,6 +21,7 @@ public class PathBindingEvaluator {
|
||||
private final VariableTracer variableTracer;
|
||||
private final ConstantResolver constantResolver;
|
||||
private final TypeResolver typeResolver;
|
||||
private final Map<String, Map<String, String>> traceBindingsCache = new HashMap<>();
|
||||
|
||||
public PathBindingEvaluator(
|
||||
CodebaseContext context,
|
||||
@@ -34,10 +36,26 @@ public class PathBindingEvaluator {
|
||||
|
||||
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
|
||||
Map<String, String> traceBindings(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
||||
Map<String, String> bindings = new HashMap<>();
|
||||
if (path == null || path.size() < 2) {
|
||||
return bindings;
|
||||
return Map.of();
|
||||
}
|
||||
String cacheKey = pathCacheKey(path);
|
||||
Map<String, String> cached = traceBindingsCache.get(cacheKey);
|
||||
if (cached != null) {
|
||||
return new HashMap<>(cached);
|
||||
}
|
||||
Map<String, String> bindings = computeTraceBindings(path, callGraph, pathFinder);
|
||||
traceBindingsCache.put(cacheKey, Map.copyOf(bindings));
|
||||
return new HashMap<>(bindings);
|
||||
}
|
||||
|
||||
public void clearAnalysisCaches() {
|
||||
traceBindingsCache.clear();
|
||||
}
|
||||
|
||||
private Map<String, String> computeTraceBindings(
|
||||
List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
||||
Map<String, String> bindings = new HashMap<>();
|
||||
for (int i = 0; i < path.size() - 1; i++) {
|
||||
String caller = path.get(i);
|
||||
String target = path.get(i + 1);
|
||||
@@ -50,6 +68,14 @@ public class PathBindingEvaluator {
|
||||
return bindings;
|
||||
}
|
||||
|
||||
private static String pathCacheKey(List<String> path) {
|
||||
StringJoiner joiner = new StringJoiner("\0");
|
||||
for (String hop : path) {
|
||||
joiner.add(hop);
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
public boolean isPathCompatible(
|
||||
List<String> path,
|
||||
Map<String, List<CallEdge>> callGraph,
|
||||
|
||||
@@ -23,7 +23,7 @@ public class SpringMvcDetector {
|
||||
cu.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(TypeDeclaration node) {
|
||||
if (isRestController(node)) {
|
||||
if (isRestController(node) && !node.isInterface()) {
|
||||
processController(node, entryPoints);
|
||||
}
|
||||
// Support WebFlux RouterFunction beans
|
||||
|
||||
@@ -7,11 +7,21 @@ import java.util.*;
|
||||
public class TypeResolver {
|
||||
private final CodebaseContext context;
|
||||
|
||||
private static final Set<String> GENERIC_EVENT_PARAMETER_NAMES = Set.of("e", "ev");
|
||||
|
||||
public TypeResolver(CodebaseContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public int getParameterIndex(String methodFqn, String paramName) {
|
||||
return getParameterIndex(methodFqn, paramName, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@code allowSyntheticEventPlaceholder} is true, maps generic trigger placeholders
|
||||
* such as {@code e} onto the sole parameter of single-arg methods (e.g. {@code sendEvent}).
|
||||
*/
|
||||
public int getParameterIndex(String methodFqn, String paramName, boolean allowSyntheticEventPlaceholder) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return -1;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
@@ -25,11 +35,20 @@ public class TypeResolver {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (allowSyntheticEventPlaceholder
|
||||
&& md.parameters().size() == 1
|
||||
&& isGenericEventParameterName(paramName)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean isGenericEventParameterName(String paramName) {
|
||||
return paramName != null && GENERIC_EVENT_PARAMETER_NAMES.contains(paramName);
|
||||
}
|
||||
|
||||
public List<String> getParameterNames(String methodFqn) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||
return null;
|
||||
|
||||
@@ -7,6 +7,7 @@ import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.*;
|
||||
|
||||
@@ -16,6 +17,7 @@ public class VariableTracer {
|
||||
private ConstantExtractor constantExtractor;
|
||||
|
||||
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
|
||||
private final Map<String, String> variableDeclaredTypeCache = new HashMap<>();
|
||||
|
||||
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
|
||||
this.context = context;
|
||||
@@ -27,6 +29,13 @@ public class VariableTracer {
|
||||
this.constantExtractor = constantExtractor;
|
||||
}
|
||||
|
||||
public void clearAnalysisCaches() {
|
||||
variableDeclaredTypeCache.clear();
|
||||
if (dataFlowModel instanceof JdtDataFlowModel jdtDataFlowModel) {
|
||||
jdtDataFlowModel.clearAnalysisCaches();
|
||||
}
|
||||
}
|
||||
|
||||
public Expression traceVariable(Expression expr) {
|
||||
List<Expression> all = traceVariableAll(expr);
|
||||
return all.isEmpty() ? expr : all.get(all.size() - 1);
|
||||
@@ -55,79 +64,48 @@ public class VariableTracer {
|
||||
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;
|
||||
String setterMethodName;
|
||||
String indexedFieldName;
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md == null || md.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isBeanStyleAccessorName(getterName)) {
|
||||
propName = propertyNameFromAccessor(getterName);
|
||||
setterMethodName = "set" + capitalizeProperty(propName);
|
||||
indexedFieldName = propName;
|
||||
String setterMethodName;
|
||||
String indexedFieldName;
|
||||
if (isBeanStyleAccessorName(getterName)) {
|
||||
String propName = propertyNameFromAccessor(getterName);
|
||||
setterMethodName = "set" + capitalizeProperty(propName);
|
||||
indexedFieldName = propName;
|
||||
|
||||
String varTypeName = getVariableDeclaredType(methodFqn, varName);
|
||||
if (varTypeName != null) {
|
||||
CompilationUnit contextCu = md.getRoot() instanceof CompilationUnit cu ? cu : null;
|
||||
TypeDeclaration varType = contextCu != null
|
||||
? context.getTypeDeclaration(varTypeName, contextCu)
|
||||
: null;
|
||||
if (varType == null) {
|
||||
varType = context.getTypeDeclaration(varTypeName);
|
||||
}
|
||||
if (varType != null) {
|
||||
Optional<AccessorSummary> indexedSetter = context.getAccessorIndex()
|
||||
.lookup(context.getFqn(varType), setterMethodName);
|
||||
if (indexedSetter.isPresent() && indexedSetter.get().isSetter()) {
|
||||
setterMethodName = indexedSetter.get().methodName();
|
||||
indexedFieldName = indexedSetter.get().fieldName();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||
setterMethodName = "set" + propName;
|
||||
indexedFieldName = propName;
|
||||
String varTypeName = getVariableDeclaredType(methodFqn, varName);
|
||||
if (varTypeName != null) {
|
||||
CompilationUnit contextCu = md.getRoot() instanceof CompilationUnit cu ? cu : null;
|
||||
TypeDeclaration varType = contextCu != null
|
||||
? context.getTypeDeclaration(varTypeName, contextCu)
|
||||
: null;
|
||||
if (varType == null) {
|
||||
varType = context.getTypeDeclaration(varTypeName);
|
||||
}
|
||||
|
||||
final String resolvedSetterName = setterMethodName;
|
||||
final String resolvedFieldName = indexedFieldName;
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if (node.getName().getIdentifier().equals(resolvedSetterName)
|
||||
|| node.getName().getIdentifier().equalsIgnoreCase("set" + resolvedFieldName)
|
||||
|| node.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
||||
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);
|
||||
if (varType != null) {
|
||||
Optional<AccessorSummary> indexedSetter = context.getAccessorIndex()
|
||||
.lookup(context.getFqn(varType), setterMethodName);
|
||||
if (indexedSetter.isPresent() && indexedSetter.get().isSetter()) {
|
||||
setterMethodName = indexedSetter.get().methodName();
|
||||
indexedFieldName = indexedSetter.get().fieldName();
|
||||
}
|
||||
@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(resolvedFieldName)) {
|
||||
setterArg[0] = node.getRightHandSide();
|
||||
}
|
||||
}
|
||||
} else if (node.getLeftHandSide() instanceof QualifiedName qqn) {
|
||||
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
||||
if (qqn.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
||||
setterArg[0] = node.getRightHandSide();
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return setterArg[0];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||
setterMethodName = "set" + propName;
|
||||
indexedFieldName = propName;
|
||||
}
|
||||
|
||||
if (dataFlowModel instanceof JdtDataFlowModel jdtDataFlowModel) {
|
||||
return jdtDataFlowModel.findLocalSetterArgument(
|
||||
md, varName, getterName, setterMethodName, indexedFieldName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -159,9 +137,18 @@ public class VariableTracer {
|
||||
}
|
||||
|
||||
private String getFieldType(TypeDeclaration td, String fieldName, CodebaseContext context, java.util.Set<String> visited) {
|
||||
if (td == null) return null;
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
String typeFqn = context.getFqn(td);
|
||||
if (!visited.add(typeFqn)) return null;
|
||||
if (!visited.add(typeFqn)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<String> indexed = context.getFieldTypeIndex().lookup(typeFqn, fieldName);
|
||||
if (indexed.isPresent()) {
|
||||
return indexed.get();
|
||||
}
|
||||
|
||||
for (FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
@@ -215,7 +202,21 @@ public class VariableTracer {
|
||||
}
|
||||
|
||||
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
|
||||
if (methodFqn == null) return null;
|
||||
if (methodFqn == null) {
|
||||
return null;
|
||||
}
|
||||
String cacheKey = methodFqn + "|" + cleanFieldName;
|
||||
String cached = variableDeclaredTypeCache.get(cacheKey);
|
||||
if (cached != null) {
|
||||
return cached.isEmpty() ? null : cached;
|
||||
}
|
||||
|
||||
String resolved = resolveVariableDeclaredTypeUncached(methodFqn, cleanFieldName);
|
||||
variableDeclaredTypeCache.put(cacheKey, resolved == null ? "" : resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String resolveVariableDeclaredTypeUncached(String methodFqn, String cleanFieldName) {
|
||||
int lastDot = methodFqn.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
String fqn = methodFqn.substring(0, lastDot);
|
||||
@@ -336,19 +337,6 @@ public class VariableTracer {
|
||||
String fieldType = getFieldType(td, cleanFieldName, context, new java.util.HashSet<>());
|
||||
if (fieldType != null) return fieldType;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,17 @@ package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndex;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndexBuilder;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.FieldTypeIndex;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.FieldTypeIndexBuilder;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.GetterChainIndex;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.GetterChainIndexBuilder;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.IndexedFieldConstantsIndex;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.IndexedFieldConstantsIndexBuilder;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.PolymorphicAccessorIndex;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.PolymorphicAccessorIndexBuilder;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.PolymorphicMethodExpansionIndex;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.PolymorphicMethodExpansionIndexBuilder;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ClassCompatibilityCache;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
|
||||
@@ -20,6 +31,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -28,6 +40,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class CodebaseContext {
|
||||
private final Map<String, CompilationUnit> classes = new HashMap<>();
|
||||
private final Map<String, TypeDeclaration> typeDeclarations = new HashMap<>();
|
||||
private final Map<String, org.eclipse.jdt.core.dom.RecordDeclaration> recordDeclarations = new HashMap<>();
|
||||
private final Map<String, Path> classPaths = new HashMap<>();
|
||||
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
||||
@@ -46,6 +59,16 @@ public class CodebaseContext {
|
||||
private Path projectRoot;
|
||||
private final Map<String, Object> cache = new HashMap<>();
|
||||
private AccessorIndex accessorIndex = AccessorIndex.empty();
|
||||
private PolymorphicAccessorIndex polymorphicAccessorIndex = PolymorphicAccessorIndex.empty();
|
||||
private GetterChainIndex getterChainIndex = GetterChainIndex.empty();
|
||||
private IndexedFieldConstantsIndex indexedFieldConstantsIndex = IndexedFieldConstantsIndex.empty();
|
||||
private FieldTypeIndex fieldTypeIndex = FieldTypeIndex.empty();
|
||||
private PolymorphicMethodExpansionIndex polymorphicMethodExpansionIndex =
|
||||
PolymorphicMethodExpansionIndex.empty();
|
||||
private final ClassCompatibilityCache classCompatibilityCache = new ClassCompatibilityCache();
|
||||
private final Map<String, MethodDeclaration> methodDeclarationCache = new HashMap<>();
|
||||
private final Map<String, String> superclassFqnCache = new HashMap<>();
|
||||
private final Map<String, List<String>> implementationsCache = new HashMap<>();
|
||||
private boolean inlineAccessors = true;
|
||||
|
||||
public Map<String, Object> getCache() {
|
||||
@@ -56,6 +79,40 @@ public class CodebaseContext {
|
||||
return accessorIndex;
|
||||
}
|
||||
|
||||
public PolymorphicAccessorIndex getPolymorphicAccessorIndex() {
|
||||
return polymorphicAccessorIndex;
|
||||
}
|
||||
|
||||
public GetterChainIndex getGetterChainIndex() {
|
||||
return getterChainIndex;
|
||||
}
|
||||
|
||||
public IndexedFieldConstantsIndex getIndexedFieldConstantsIndex() {
|
||||
return indexedFieldConstantsIndex;
|
||||
}
|
||||
|
||||
public FieldTypeIndex getFieldTypeIndex() {
|
||||
return fieldTypeIndex;
|
||||
}
|
||||
|
||||
public PolymorphicMethodExpansionIndex getPolymorphicMethodExpansionIndex() {
|
||||
return polymorphicMethodExpansionIndex;
|
||||
}
|
||||
|
||||
public ClassCompatibilityCache getClassCompatibilityCache() {
|
||||
return classCompatibilityCache;
|
||||
}
|
||||
|
||||
public void clearAnalysisCaches() {
|
||||
methodDeclarationCache.clear();
|
||||
superclassFqnCache.clear();
|
||||
classCompatibilityCache.clear();
|
||||
}
|
||||
|
||||
public Set<String> getIndexedTypeFqns() {
|
||||
return Collections.unmodifiableSet(classes.keySet());
|
||||
}
|
||||
|
||||
public boolean isInlineAccessors() {
|
||||
return inlineAccessors;
|
||||
}
|
||||
@@ -240,6 +297,7 @@ public class CodebaseContext {
|
||||
|
||||
List<Path> javaFiles = FileUtils.findFilesWithExtension(rootDirs, ".java", activeIgnore);
|
||||
log.info("CodebaseContext found {} java files across {} root directories", javaFiles.size(), rootDirs.size());
|
||||
typeDeclarations.clear();
|
||||
for (Path javaFile : javaFiles) {
|
||||
String source = Files.readString(javaFile);
|
||||
CompilationUnit cu = parse(source, javaFile.toAbsolutePath().toString());
|
||||
@@ -257,9 +315,22 @@ public class CodebaseContext {
|
||||
}
|
||||
}
|
||||
|
||||
this.implementationsCache.clear();
|
||||
this.accessorIndex = inlineAccessors
|
||||
? new AccessorIndexBuilder().build(this)
|
||||
: AccessorIndex.empty();
|
||||
this.polymorphicAccessorIndex = inlineAccessors
|
||||
? new PolymorphicAccessorIndexBuilder().build(this, accessorIndex)
|
||||
: PolymorphicAccessorIndex.empty();
|
||||
this.getterChainIndex = inlineAccessors
|
||||
? new GetterChainIndexBuilder().build(this, accessorIndex)
|
||||
: GetterChainIndex.empty();
|
||||
this.indexedFieldConstantsIndex = inlineAccessors
|
||||
? new IndexedFieldConstantsIndexBuilder().build(this, accessorIndex)
|
||||
: IndexedFieldConstantsIndex.empty();
|
||||
this.fieldTypeIndex = new FieldTypeIndexBuilder().build(this);
|
||||
this.polymorphicMethodExpansionIndex = new PolymorphicMethodExpansionIndexBuilder().build(this);
|
||||
clearAnalysisCaches();
|
||||
if (inlineAccessors) {
|
||||
log.info("Built accessor index: {} trivial accessors across {} types",
|
||||
accessorIndex.trivialCount(), accessorIndex.typeCount());
|
||||
@@ -274,6 +345,7 @@ public class CodebaseContext {
|
||||
|
||||
classes.put(fqn, (CompilationUnit) td.getRoot());
|
||||
classPaths.put(fqn, javaFile);
|
||||
typeDeclarations.put(fqn, td);
|
||||
|
||||
if (simpleNameToFqn.containsKey(simpleName)) {
|
||||
String existingFqn = simpleNameToFqn.get(simpleName);
|
||||
@@ -342,13 +414,21 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
public List<String> getImplementations(String interfaceName) {
|
||||
String cleanName = interfaceName;
|
||||
if (interfaceName != null && interfaceName.contains("<")) {
|
||||
cleanName = interfaceName.substring(0, interfaceName.indexOf('<'));
|
||||
if (interfaceName == null) {
|
||||
return List.of();
|
||||
}
|
||||
Set<String> allImpls = new HashSet<>();
|
||||
String cleanName = interfaceName.contains("<")
|
||||
? interfaceName.substring(0, interfaceName.indexOf('<'))
|
||||
: interfaceName;
|
||||
return implementationsCache.computeIfAbsent(cleanName, this::computeImplementations);
|
||||
}
|
||||
|
||||
private List<String> computeImplementations(String cleanName) {
|
||||
Set<String> allImpls = new LinkedHashSet<>();
|
||||
collectImplementations(cleanName, allImpls, new HashSet<>());
|
||||
return new ArrayList<>(allImpls);
|
||||
List<String> sorted = new ArrayList<>(allImpls);
|
||||
Collections.sort(sorted);
|
||||
return Collections.unmodifiableList(sorted);
|
||||
}
|
||||
|
||||
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
|
||||
@@ -462,10 +542,20 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
public TypeDeclaration getTypeDeclaration(String name) {
|
||||
TypeDeclaration cached = typeDeclarations.get(name);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Try exact FQN match first
|
||||
CompilationUnit cu = classes.get(name);
|
||||
if (cu != null)
|
||||
return findTypeInCu(cu, name);
|
||||
if (cu != null) {
|
||||
TypeDeclaration resolved = findTypeInCu(cu, name);
|
||||
if (resolved != null) {
|
||||
typeDeclarations.put(name, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// If it's a simple name, check if it's ambiguous
|
||||
if (ambiguousSimpleNames.contains(name)) {
|
||||
@@ -476,9 +566,18 @@ public class CodebaseContext {
|
||||
// If not ambiguous, return the one we found during scan
|
||||
String fqn = simpleNameToFqn.get(name);
|
||||
if (fqn != null) {
|
||||
cached = typeDeclarations.get(fqn);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
cu = classes.get(fqn);
|
||||
if (cu != null)
|
||||
return findTypeInCu(cu, fqn);
|
||||
if (cu != null) {
|
||||
TypeDeclaration resolved = findTypeInCu(cu, fqn);
|
||||
if (resolved != null) {
|
||||
typeDeclarations.put(fqn, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -538,7 +637,21 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
public String getSuperclassFqn(TypeDeclaration td) {
|
||||
if (td == null) return null;
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
String typeFqn = getFqn(td);
|
||||
if (typeFqn != null && superclassFqnCache.containsKey(typeFqn)) {
|
||||
return superclassFqnCache.get(typeFqn);
|
||||
}
|
||||
String resolved = resolveSuperclassFqn(td);
|
||||
if (typeFqn != null) {
|
||||
superclassFqnCache.put(typeFqn, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String resolveSuperclassFqn(TypeDeclaration td) {
|
||||
ITypeBinding binding = td.resolveBinding();
|
||||
if (binding != null) {
|
||||
ITypeBinding superBinding = binding.getSuperclass();
|
||||
@@ -568,7 +681,23 @@ public class CodebaseContext {
|
||||
}
|
||||
|
||||
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||
if (td == null) return null;
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
String typeFqn = getFqn(td);
|
||||
if (typeFqn != null) {
|
||||
String cacheKey = typeFqn + "#" + methodName + "#" + includeSuper;
|
||||
if (methodDeclarationCache.containsKey(cacheKey)) {
|
||||
return methodDeclarationCache.get(cacheKey);
|
||||
}
|
||||
MethodDeclaration resolved = findMethodDeclarationUncached(td, methodName, includeSuper);
|
||||
methodDeclarationCache.put(cacheKey, resolved);
|
||||
return resolved;
|
||||
}
|
||||
return findMethodDeclarationUncached(td, methodName, includeSuper);
|
||||
}
|
||||
|
||||
private MethodDeclaration findMethodDeclarationUncached(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||
ITypeBinding binding = td.resolveBinding();
|
||||
if (binding != null && includeSuper) {
|
||||
IMethodBinding methodBinding = findMethodBinding(binding, methodName);
|
||||
@@ -647,6 +776,44 @@ public class CodebaseContext {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean areClassesPolymorphicallyCompatible(String classNeighbor, String classTarget) {
|
||||
if (classNeighbor == null || classTarget == null) {
|
||||
return false;
|
||||
}
|
||||
if (classNeighbor.equals(classTarget)) {
|
||||
return true;
|
||||
}
|
||||
String simpleClassNeighbor = simpleTypeName(classNeighbor);
|
||||
String simpleClassTarget = simpleTypeName(classTarget);
|
||||
if (simpleClassNeighbor.equals(simpleClassTarget)) {
|
||||
return true;
|
||||
}
|
||||
return classCompatibilityCache.areClassesCompatible(
|
||||
classNeighbor,
|
||||
classTarget,
|
||||
this::computePolymorphicClassCompatibility);
|
||||
}
|
||||
|
||||
private boolean computePolymorphicClassCompatibility(String classNeighbor, String classTarget) {
|
||||
List<String> targetImplementations = getImplementations(classTarget);
|
||||
for (String implementation : targetImplementations) {
|
||||
if (implementation.equals(classNeighbor) || implementation.endsWith("." + classNeighbor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
List<String> neighborImplementations = getImplementations(classNeighbor);
|
||||
for (String implementation : neighborImplementations) {
|
||||
if (implementation.equals(classTarget) || implementation.endsWith("." + classTarget)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String simpleTypeName(String typeFqn) {
|
||||
return typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
|
||||
}
|
||||
|
||||
public org.eclipse.jdt.core.dom.RecordDeclaration getRecordDeclaration(String fqn) {
|
||||
if (fqn == null || fqn.isEmpty()) {
|
||||
return null;
|
||||
|
||||
@@ -14,6 +14,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
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<>();
|
||||
private final Map<String, Set<String>> compatibleContextTypesCache = new HashMap<>();
|
||||
|
||||
private static final ThreadLocal<List<String>> CURRENT_PATH = new ThreadLocal<>();
|
||||
|
||||
@@ -33,6 +34,10 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void clearAnalysisCaches() {
|
||||
compatibleContextTypesCache.clear();
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode current = node;
|
||||
while (current != null) {
|
||||
@@ -372,6 +377,11 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
if (concreteAccessor.isPresent()) {
|
||||
accessor = concreteAccessor;
|
||||
} else if (shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
|
||||
List<Expression> concreteOverride = inlineConcreteMethodOverride(
|
||||
methodName, receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
||||
if (!concreteOverride.isEmpty()) {
|
||||
return concreteOverride;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@@ -457,6 +467,42 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<Expression> inlineConcreteMethodOverride(
|
||||
String methodName,
|
||||
List<Expression> receiverDefs,
|
||||
Set<ASTNode> visited,
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||
int depth) {
|
||||
List<Expression> results = new ArrayList<>();
|
||||
for (Expression receiverDef : receiverDefs) {
|
||||
String concreteFqn = concreteReceiverTypeFqn(receiverDef);
|
||||
if (concreteFqn == null) {
|
||||
continue;
|
||||
}
|
||||
TypeDeclaration concreteType = context.getTypeDeclaration(concreteFqn);
|
||||
if (concreteType == null) {
|
||||
continue;
|
||||
}
|
||||
MethodDeclaration method = context.findMethodDeclaration(concreteType, methodName, true);
|
||||
if (method == null || method.getBody() == null) {
|
||||
continue;
|
||||
}
|
||||
for (Object statement : method.getBody().statements()) {
|
||||
if (statement instanceof ReturnStatement returnStatement
|
||||
&& returnStatement.getExpression() != null) {
|
||||
results.addAll(getReachingDefinitions(
|
||||
returnStatement.getExpression(),
|
||||
visited,
|
||||
paramBindings,
|
||||
instanceFieldBindings,
|
||||
depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private String concreteReceiverTypeFqn(Expression receiverDef) {
|
||||
if (receiverDef instanceof ClassInstanceCreation cic) {
|
||||
return AccessorInlining.resolveTypeFqn(cic);
|
||||
@@ -1449,6 +1495,272 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
return returns;
|
||||
}
|
||||
|
||||
public Expression findLocalSetterArgument(
|
||||
MethodDeclaration md,
|
||||
String varName,
|
||||
String getterName,
|
||||
String resolvedSetterName,
|
||||
String resolvedFieldName) {
|
||||
if (md == null || md.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ControlFlowGraph cfg = cfgCache.computeIfAbsent(md, CfgBuilder::build);
|
||||
MethodInvocation getterUse = findGetterUseOnVariable(cfg, varName, getterName);
|
||||
|
||||
List<ControlFlowGraph.CfgNode> nodesToScan;
|
||||
ControlFlowGraph.CfgNode getterCfgNode = null;
|
||||
if (getterUse != null && getterUse.getExpression() instanceof SimpleName receiver) {
|
||||
getterCfgNode = findCfgNode(cfg, getterUse);
|
||||
nodesToScan = collectCfgNodesBetweenDefAndUse(md, receiver, getterUse, cfg);
|
||||
} else {
|
||||
nodesToScan = orderedStatementNodes(cfg);
|
||||
}
|
||||
|
||||
Expression lastSetterArg = null;
|
||||
for (ControlFlowGraph.CfgNode node : nodesToScan) {
|
||||
if (getterCfgNode != null && node == getterCfgNode) {
|
||||
continue;
|
||||
}
|
||||
Expression arg = extractSetterArgument(node.getAstNode(), varName, resolvedSetterName, resolvedFieldName);
|
||||
if (arg != null) {
|
||||
lastSetterArg = arg;
|
||||
}
|
||||
}
|
||||
return lastSetterArg;
|
||||
}
|
||||
|
||||
private List<ControlFlowGraph.CfgNode> orderedStatementNodes(ControlFlowGraph cfg) {
|
||||
List<ControlFlowGraph.CfgNode> result = new ArrayList<>();
|
||||
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||
if (node.getType() == ControlFlowGraph.CfgNodeType.STATEMENT && node.getAstNode() != null) {
|
||||
result.add(node);
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparingInt(cfg.getNodes()::indexOf));
|
||||
return result;
|
||||
}
|
||||
|
||||
private MethodInvocation findGetterUseOnVariable(ControlFlowGraph cfg, String varName, String getterName) {
|
||||
if (getterName == null || getterName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MethodInvocation lastMatch = null;
|
||||
for (ControlFlowGraph.CfgNode node : orderedStatementNodes(cfg)) {
|
||||
MethodInvocation match = findGetterInvocationInNode(node.getAstNode(), varName, getterName);
|
||||
if (match != null) {
|
||||
lastMatch = match;
|
||||
}
|
||||
}
|
||||
return lastMatch;
|
||||
}
|
||||
|
||||
private MethodInvocation findGetterInvocationInNode(ASTNode node, String varName, String getterName) {
|
||||
Expression expr = unwrapCfgStatementExpression(node);
|
||||
if (expr == null) {
|
||||
return null;
|
||||
}
|
||||
final MethodInvocation[] found = new MethodInvocation[1];
|
||||
if (expr instanceof MethodInvocation mi && matchesGetterInvocation(mi, varName, getterName)) {
|
||||
found[0] = mi;
|
||||
}
|
||||
expr.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation nested) {
|
||||
if (nested != expr && matchesGetterInvocation(nested, varName, getterName)) {
|
||||
found[0] = nested;
|
||||
}
|
||||
return super.visit(nested);
|
||||
}
|
||||
});
|
||||
return found[0];
|
||||
}
|
||||
|
||||
private boolean matchesGetterInvocation(MethodInvocation mi, String varName, String getterName) {
|
||||
if (!(mi.getExpression() instanceof SimpleName sn) || !sn.getIdentifier().equals(varName)) {
|
||||
return false;
|
||||
}
|
||||
String methodId = mi.getName().getIdentifier();
|
||||
if (methodId.equals(getterName) || methodId.equalsIgnoreCase(getterName)) {
|
||||
return true;
|
||||
}
|
||||
if (!getterName.startsWith("get") && !getterName.startsWith("is")) {
|
||||
return methodId.equals("get" + getterName) || methodId.equalsIgnoreCase("get" + getterName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<ControlFlowGraph.CfgNode> collectCfgNodesBetweenDefAndUse(
|
||||
MethodDeclaration md,
|
||||
SimpleName receiver,
|
||||
MethodInvocation useSite,
|
||||
ControlFlowGraph cfg) {
|
||||
ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
|
||||
if (rd == null) {
|
||||
return orderedStatementNodes(cfg);
|
||||
}
|
||||
|
||||
ControlFlowGraph.CfgNode useCfgNode = findCfgNode(cfg, useSite);
|
||||
if (useCfgNode == null) {
|
||||
return orderedStatementNodes(cfg);
|
||||
}
|
||||
|
||||
Set<ControlFlowGraph.CfgNode> pathNodes = new HashSet<>();
|
||||
Set<ASTNode> defs = rd.getReachingDefinitions(receiver, receiver.getIdentifier());
|
||||
for (ASTNode def : defs) {
|
||||
ControlFlowGraph.CfgNode defCfgNode = findCfgNode(cfg, def);
|
||||
if (defCfgNode == null) {
|
||||
continue;
|
||||
}
|
||||
Set<ControlFlowGraph.CfgNode> reachFromDef = forwardReach(defCfgNode);
|
||||
Set<ControlFlowGraph.CfgNode> reachToUse = backwardReach(useCfgNode);
|
||||
Set<ControlFlowGraph.CfgNode> between = new HashSet<>(reachFromDef);
|
||||
between.retainAll(reachToUse);
|
||||
pathNodes.addAll(between);
|
||||
}
|
||||
|
||||
if (pathNodes.isEmpty()) {
|
||||
return orderedStatementNodes(cfg);
|
||||
}
|
||||
|
||||
List<ControlFlowGraph.CfgNode> ordered = new ArrayList<>();
|
||||
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||
if (pathNodes.contains(node)) {
|
||||
ordered.add(node);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private Set<ControlFlowGraph.CfgNode> forwardReach(ControlFlowGraph.CfgNode start) {
|
||||
Set<ControlFlowGraph.CfgNode> reached = new HashSet<>();
|
||||
Queue<ControlFlowGraph.CfgNode> queue = new LinkedList<>();
|
||||
queue.add(start);
|
||||
reached.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
ControlFlowGraph.CfgNode current = queue.poll();
|
||||
for (ControlFlowGraph.CfgNode succ : current.getSuccessors()) {
|
||||
if (reached.add(succ)) {
|
||||
queue.add(succ);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
|
||||
private Set<ControlFlowGraph.CfgNode> backwardReach(ControlFlowGraph.CfgNode end) {
|
||||
Set<ControlFlowGraph.CfgNode> reached = new HashSet<>();
|
||||
Queue<ControlFlowGraph.CfgNode> queue = new LinkedList<>();
|
||||
queue.add(end);
|
||||
reached.add(end);
|
||||
while (!queue.isEmpty()) {
|
||||
ControlFlowGraph.CfgNode current = queue.poll();
|
||||
for (ControlFlowGraph.CfgNode pred : current.getPredecessors()) {
|
||||
if (reached.add(pred)) {
|
||||
queue.add(pred);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
|
||||
private Expression extractSetterArgument(
|
||||
ASTNode node,
|
||||
String varName,
|
||||
String resolvedSetterName,
|
||||
String resolvedFieldName) {
|
||||
Expression expr = unwrapCfgStatementExpression(node);
|
||||
if (expr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Expression direct = matchSetterExpression(expr, varName, resolvedSetterName, resolvedFieldName);
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
if (expr instanceof Assignment assignment) {
|
||||
return matchAssignmentSetter(assignment, varName, resolvedFieldName);
|
||||
}
|
||||
|
||||
final Expression[] found = new Expression[1];
|
||||
expr.accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation nested) {
|
||||
Expression arg = matchSetterExpression(nested, varName, resolvedSetterName, resolvedFieldName);
|
||||
if (arg != null) {
|
||||
found[0] = arg;
|
||||
}
|
||||
return super.visit(nested);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(Assignment assignment) {
|
||||
Expression arg = matchAssignmentSetter(assignment, varName, resolvedFieldName);
|
||||
if (arg != null) {
|
||||
found[0] = arg;
|
||||
}
|
||||
return super.visit(assignment);
|
||||
}
|
||||
});
|
||||
return found[0];
|
||||
}
|
||||
|
||||
private Expression unwrapCfgStatementExpression(ASTNode node) {
|
||||
if (node instanceof ExpressionStatement es) {
|
||||
return es.getExpression();
|
||||
}
|
||||
if (node instanceof VariableDeclarationStatement vds && !vds.fragments().isEmpty()) {
|
||||
VariableDeclarationFragment frag = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||
return frag.getInitializer();
|
||||
}
|
||||
if (node instanceof Assignment assignment) {
|
||||
return assignment;
|
||||
}
|
||||
if (node instanceof Expression expr) {
|
||||
return expr;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression matchSetterExpression(
|
||||
Expression expr,
|
||||
String varName,
|
||||
String resolvedSetterName,
|
||||
String resolvedFieldName) {
|
||||
if (!(expr instanceof MethodInvocation mi)) {
|
||||
return null;
|
||||
}
|
||||
String methodId = mi.getName().getIdentifier();
|
||||
if (!methodId.equals(resolvedSetterName)
|
||||
&& !methodId.equalsIgnoreCase("set" + resolvedFieldName)
|
||||
&& !methodId.equalsIgnoreCase(resolvedFieldName)) {
|
||||
return null;
|
||||
}
|
||||
if (mi.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)
|
||||
&& !mi.arguments().isEmpty()) {
|
||||
return (Expression) mi.arguments().get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Expression matchAssignmentSetter(Assignment assignment, String varName, String resolvedFieldName) {
|
||||
Expression lhs = assignment.getLeftHandSide();
|
||||
if (lhs instanceof FieldAccess fa) {
|
||||
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
||||
if (fa.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
||||
return assignment.getRightHandSide();
|
||||
}
|
||||
}
|
||||
} else if (lhs instanceof QualifiedName qqn) {
|
||||
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
||||
if (qqn.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
||||
return assignment.getRightHandSide();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||
if (expr == null) return null;
|
||||
@@ -1473,6 +1785,12 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
}
|
||||
|
||||
private Set<String> getCompatibleContextTypes(List<String> currentPath, String declaringClassFqn) {
|
||||
String cacheKey = String.valueOf(currentPath) + "#" + declaringClassFqn;
|
||||
return compatibleContextTypesCache.computeIfAbsent(
|
||||
cacheKey, ignored -> computeCompatibleContextTypes(currentPath, declaringClassFqn));
|
||||
}
|
||||
|
||||
private Set<String> computeCompatibleContextTypes(List<String> currentPath, String declaringClassFqn) {
|
||||
Set<String> contextTypes = new HashSet<>();
|
||||
List<String> implementations = context.getImplementations(declaringClassFqn);
|
||||
|
||||
|
||||
@@ -45,4 +45,44 @@ class MachineScopeFilterEntryPointTest {
|
||||
.contains("POST /api/orders/pay", "POST /api/commands/{commandKey}")
|
||||
.doesNotContain("POST /api/documents/submit");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepDedicatedOrderEndpointOnStandardOrderMachineInMultiModuleCodebase() {
|
||||
EntryPoint orderPay = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.REST)
|
||||
.name("POST /api/orders/pay")
|
||||
.className("click.kamil.examples.statemachine.layered.web.OrderController")
|
||||
.methodName("pay")
|
||||
.metadata(Map.of("path", "/api/orders/pay"))
|
||||
.build();
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
List<EntryPoint> scoped = MachineScopeFilter.filterEntryPointsForMachine(
|
||||
List.of(orderPay),
|
||||
"click.kamil.examples.statemachine.layered.order.config.StandardOrderStateMachineConfiguration",
|
||||
context);
|
||||
|
||||
assertThat(scoped).extracting(EntryPoint::getName)
|
||||
.containsExactly("POST /api/orders/pay");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepOrdersPathOnNonDomainNamedMachineConfig() {
|
||||
EntryPoint ordersSubmit = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.REST)
|
||||
.name("POST /api/v2/orders/submit")
|
||||
.className("click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl")
|
||||
.methodName("submitOrder")
|
||||
.metadata(Map.of("path", "/api/v2/orders/submit"))
|
||||
.build();
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
List<EntryPoint> scoped = MachineScopeFilter.filterEntryPointsForMachine(
|
||||
List.of(ordersSubmit),
|
||||
"click.kamil.examples.statemachine.inheritance.config.InheritanceStateMachineConfig",
|
||||
context);
|
||||
|
||||
assertThat(scoped).extracting(EntryPoint::getName)
|
||||
.containsExactly("POST /api/v2/orders/submit");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
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;
|
||||
@@ -9,6 +10,7 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -165,4 +167,37 @@ public class HeuristicBeanResolutionEngineTest {
|
||||
engine.isRoutedToCorrectMachine(chain, "com.example.DocumentStateMachineConfig", context),
|
||||
"Ambiguous shared gateway should not attach to every machine");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptTriggerWhenSpringErasureDiffersFromStringMachineTypes(@TempDir Path tempDir) throws Exception {
|
||||
Files.writeString(tempDir.resolve("InheritanceStateMachineConfig.java"), """
|
||||
package com.example;
|
||||
@org.springframework.statemachine.config.EnableStateMachine
|
||||
public class InheritanceStateMachineConfig
|
||||
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
|
||||
""");
|
||||
Files.writeString(tempDir.resolve("ProcessingServiceImpl.java"), """
|
||||
package com.example;
|
||||
public class ProcessingServiceImpl {
|
||||
public void doProcess() {}
|
||||
}
|
||||
""");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
CallChain triggerProbe = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.ProcessingServiceImpl")
|
||||
.methodName("doProcess")
|
||||
.event("INHERITED_SUBMIT")
|
||||
.stateTypeFqn("java.lang.Object")
|
||||
.eventTypeFqn("java.lang.Object")
|
||||
.build())
|
||||
.methodChain(List.of())
|
||||
.build();
|
||||
|
||||
assertTrue(engine.isRoutedToCorrectMachine(
|
||||
triggerProbe, "com.example.InheritanceStateMachineConfig", context));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class AnalysisPerformanceCacheTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
CodebaseContext context;
|
||||
ConstantExtractor constantExtractor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||
ConstantResolver constantResolver = context.getConstantResolver();
|
||||
constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
|
||||
constantExtractor.setConstructorAnalyzer(new ConstructorAnalyzer(context, constantResolver));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrecomputeIndexedFieldConstants() throws IOException {
|
||||
writeJava("com/example/Payload.java", """
|
||||
package com.example;
|
||||
public class Payload {
|
||||
private String event = "PAY";
|
||||
public String getEvent() { return event; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> precomputed = context.getIndexedFieldConstantsIndex()
|
||||
.lookup("com.example.Payload", "getEvent")
|
||||
.orElseThrow();
|
||||
assertThat(precomputed).containsExactly("PAY");
|
||||
|
||||
List<String> first = constantExtractor.resolveMethodReturnConstant(
|
||||
"com.example.Payload", "getEvent", 0, new HashSet<>(), null);
|
||||
List<String> second = constantExtractor.resolveMethodReturnConstant(
|
||||
"com.example.Payload", "getEvent", 0, new HashSet<>(), null);
|
||||
assertThat(first).containsExactly("PAY");
|
||||
assertThat(second).isEqualTo(first);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findMethodDeclarationShouldBeMemoized() throws IOException {
|
||||
writeJava("com/example/Demo.java", """
|
||||
package com.example;
|
||||
public class Demo {
|
||||
public void run() {}
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
TypeDeclaration type = context.getTypeDeclaration("com.example.Demo");
|
||||
MethodDeclaration first = context.findMethodDeclaration(type, "run", true);
|
||||
MethodDeclaration second = context.findMethodDeclaration(type, "run", true);
|
||||
|
||||
assertThat(first).isNotNull();
|
||||
assertThat(second).isSameAs(first);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMemoizePolymorphicClassCompatibility() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
assertThat(context.areClassesPolymorphicallyCompatible(
|
||||
"com.example.PayEvent", "com.example.EventSource")).isTrue();
|
||||
assertThat(context.areClassesPolymorphicallyCompatible(
|
||||
"com.example.PayEvent", "com.example.EventSource")).isTrue();
|
||||
}
|
||||
|
||||
private void scan() throws IOException {
|
||||
context.scan(tempDir);
|
||||
}
|
||||
|
||||
private void writeJava(String relativePath, String source) throws IOException {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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 GetterChainIndexTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
CodebaseContext context;
|
||||
AccessorResolver accessorResolver;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||
ConstantResolver constantResolver = context.getConstantResolver();
|
||||
ConstantExtractor constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
|
||||
ConstructorAnalyzer constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
|
||||
accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrecomputeGetterChainEdgeWithFieldInitializerCic() throws IOException {
|
||||
writeJava("com/example/Payload.java", """
|
||||
package com.example;
|
||||
public class Payload {
|
||||
public String getType() { return "PAY"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/Inner.java", """
|
||||
package com.example;
|
||||
public class Inner {
|
||||
private final Payload payload = new Payload();
|
||||
public Payload getPayload() { return payload; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
GetterChainIndex index = context.getGetterChainIndex();
|
||||
assertThat(index.edgeCount()).isGreaterThan(0);
|
||||
|
||||
GetterChainEdge edge = index.lookup("com.example.Inner", "getPayload").orElseThrow();
|
||||
assertThat(edge.nextTypeFqn()).contains("Payload");
|
||||
assertThat(edge.fieldInitializerCic()).isNotNull();
|
||||
|
||||
AccessorResolver.GetterChainStep step = accessorResolver.resolveGetterChainStep(
|
||||
"com.example.Inner", "getPayload", ResolutionBudget.defaults());
|
||||
assertThat(step).isNotNull();
|
||||
assertThat(step.typeFqn()).isEqualTo(edge.nextTypeFqn());
|
||||
assertThat(step.cic()).isSameAs(edge.fieldInitializerCic());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTypeDeclarationShouldUseFlatIndex() throws IOException {
|
||||
writeJava("com/example/Demo.java", """
|
||||
package com.example;
|
||||
public class Demo {
|
||||
public String getValue() { return "X"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
TypeDeclaration first = context.getTypeDeclaration("com.example.Demo");
|
||||
TypeDeclaration second = context.getTypeDeclaration("com.example.Demo");
|
||||
|
||||
assertThat(first).isNotNull();
|
||||
assertThat(second).isSameAs(first);
|
||||
}
|
||||
|
||||
private void scan() throws IOException {
|
||||
context.scan(tempDir);
|
||||
}
|
||||
|
||||
private void writeJava(String relativePath, String source) throws IOException {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PolymorphicAccessorIndexTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
CodebaseContext context;
|
||||
ConstantExtractor constantExtractor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
context = new CodebaseContext();
|
||||
constantExtractor = new ConstantExtractor(
|
||||
context,
|
||||
new ConstantResolver(),
|
||||
mi -> null);
|
||||
constantExtractor.setConstructorAnalyzer(new ConstructorAnalyzer(context, context.getConstantResolver()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrecomputeWidenedConstantsForThreeEventImplementations() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
String getEvent();
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public String getEvent() { return "PAY"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public String getEvent() { return "SHIP"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/CancelEvent.java", """
|
||||
package com.example;
|
||||
public class CancelEvent implements EventSource {
|
||||
public String getEvent() { return "CANCEL"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
PolymorphicAccessorIndex index = context.getPolymorphicAccessorIndex();
|
||||
assertThat(index.entryCount()).isGreaterThan(0);
|
||||
|
||||
List<String> precomputed = index.widenedReturnConstants("com.example.EventSource", "getEvent")
|
||||
.orElseThrow();
|
||||
assertThat(precomputed).containsExactlyInAnyOrder("PAY", "SHIP", "CANCEL");
|
||||
|
||||
List<String> resolved = constantExtractor.resolveMethodReturnConstant(
|
||||
"com.example.EventSource", "getEvent", 0, new HashSet<>(), null);
|
||||
assertThat(resolved).containsExactlyInAnyOrder("PAY", "SHIP", "CANCEL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncludeDefaultMethodAndConcreteOverrides() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default String getEvent() { return "DEFAULT"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public String getEvent() { return "PAY"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public String getEvent() { return "SHIP"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/CancelEvent.java", """
|
||||
package com.example;
|
||||
public class CancelEvent implements EventSource {
|
||||
public String getEvent() { return "CANCEL"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> precomputed = context.getPolymorphicAccessorIndex()
|
||||
.widenedReturnConstants("com.example.EventSource", "getEvent")
|
||||
.orElseThrow();
|
||||
assertThat(precomputed).containsExactlyInAnyOrder("DEFAULT", "PAY", "SHIP", "CANCEL");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AbstractStateMachineInheritance {
|
||||
|
||||
@Test
|
||||
void shouldResolveEventsFromThreeConcreteConfigsExtendingAbstractBase() throws IOException {
|
||||
writeJava("com/example/OrderState.java", """
|
||||
package com.example;
|
||||
public enum OrderState { CREATED, PAID, SHIPPED, CANCELLED }
|
||||
""");
|
||||
writeJava("com/example/OrderEvent.java", """
|
||||
package com.example;
|
||||
public enum OrderEvent { PAY, SHIP, CANCEL }
|
||||
""");
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
OrderEvent getEvent();
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public OrderEvent getEvent() { return OrderEvent.PAY; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public OrderEvent getEvent() { return OrderEvent.SHIP; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/CancelEvent.java", """
|
||||
package com.example;
|
||||
public class CancelEvent implements EventSource {
|
||||
public OrderEvent getEvent() { return OrderEvent.CANCEL; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/BaseOrderStateMachineConfig.java", """
|
||||
package com.example;
|
||||
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
public abstract class BaseOrderStateMachineConfig
|
||||
extends StateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
||||
protected void registerTransition(
|
||||
StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions,
|
||||
EventSource source) {
|
||||
transitions.withExternal()
|
||||
.source(OrderState.CREATED)
|
||||
.target(OrderState.PAID)
|
||||
.event(source.getEvent());
|
||||
}
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayOrderStateMachineConfig.java", """
|
||||
package com.example;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
public class PayOrderStateMachineConfig extends BaseOrderStateMachineConfig {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions)
|
||||
throws Exception {
|
||||
registerTransition(transitions, new PayEvent());
|
||||
}
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipOrderStateMachineConfig.java", """
|
||||
package com.example;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
public class ShipOrderStateMachineConfig extends BaseOrderStateMachineConfig {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions)
|
||||
throws Exception {
|
||||
registerTransition(transitions, new ShipEvent());
|
||||
}
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/CancelOrderStateMachineConfig.java", """
|
||||
package com.example;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
public class CancelOrderStateMachineConfig extends BaseOrderStateMachineConfig {
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions)
|
||||
throws Exception {
|
||||
registerTransition(transitions, new CancelEvent());
|
||||
}
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> widened = context.getPolymorphicAccessorIndex()
|
||||
.widenedReturnConstants("com.example.EventSource", "getEvent")
|
||||
.orElseThrow();
|
||||
assertThat(widened).containsExactlyInAnyOrder("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
|
||||
|
||||
List<String> resolved = constantExtractor.resolveMethodReturnConstant(
|
||||
"com.example.EventSource", "getEvent", 0, new HashSet<>(), null);
|
||||
assertThat(resolved).containsExactlyInAnyOrder("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getImplementationsShouldBeMemoized() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> first = context.getImplementations("com.example.EventSource");
|
||||
List<String> second = context.getImplementations("com.example.EventSource");
|
||||
|
||||
assertThat(second).isSameAs(first);
|
||||
}
|
||||
|
||||
private void scan() throws IOException {
|
||||
context.scan(tempDir);
|
||||
}
|
||||
|
||||
private void writeJava(String relativePath, String source) throws IOException {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, source);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,8 @@ class AccessorPipelineBugFixTest {
|
||||
variableTracer = new VariableTracer(context, constantResolver);
|
||||
constantExtractor.setVariableTracer(variableTracer);
|
||||
constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
|
||||
constructorAnalyzer.setVariableTracer(variableTracer);
|
||||
constructorAnalyzer.setConstantExtractor(constantExtractor);
|
||||
accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
||||
constantExtractor.setAccessorResolver(accessorResolver);
|
||||
}
|
||||
@@ -118,6 +120,37 @@ class AccessorPipelineBugFixTest {
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("Code.A", "Code.B");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWidenToImplsEvenWhenAbstractFieldHasInlineInitializer() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { PAY, SHIP }
|
||||
""");
|
||||
writeJava("com/example/BaseEvent.java", """
|
||||
package com.example;
|
||||
public abstract class BaseEvent {
|
||||
protected Code code = Code.PAY;
|
||||
public Code getCode() { return code; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/DefaultEvent.java", """
|
||||
package com.example;
|
||||
public class DefaultEvent extends BaseEvent {}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent extends BaseEvent {
|
||||
public ShipEvent() { this.code = Code.SHIP; }
|
||||
}
|
||||
""");
|
||||
scanWorkspace();
|
||||
|
||||
List<String> resolved = constantExtractor.resolveMethodReturnConstant(
|
||||
"com.example.BaseEvent", "getCode", 0, new HashSet<>(), null);
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("Code.PAY", "Code.SHIP");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.ast.common.DataFlowModel;
|
||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InheritanceAndNestedResolutionTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
CodebaseContext context;
|
||||
ConstantExtractor constantExtractor;
|
||||
ConstructorAnalyzer constructorAnalyzer;
|
||||
AccessorResolver accessorResolver;
|
||||
VariableTracer variableTracer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||
ConstantResolver constantResolver = context.getConstantResolver();
|
||||
constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
|
||||
constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
|
||||
variableTracer = new VariableTracer(context, constantResolver);
|
||||
constantExtractor.setVariableTracer(variableTracer);
|
||||
constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
|
||||
constructorAnalyzer.setVariableTracer(variableTracer);
|
||||
constructorAnalyzer.setConstantExtractor(constantExtractor);
|
||||
accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
||||
constantExtractor.setAccessorResolver(accessorResolver);
|
||||
}
|
||||
|
||||
private void writeJava(String relativePath, String source) throws IOException {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, source);
|
||||
}
|
||||
|
||||
private void scan() throws IOException {
|
||||
context.scan(tempDir);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class InterfaceDefaultMethod {
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldIncludeDefaultMethodAndAllImpls() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { DEFAULT, PAY, SHIP }
|
||||
""");
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default Code getCode() { return Code.DEFAULT; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public Code getCode() { return Code.PAY; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public Code getCode() { return Code.SHIP; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
null, null, false, null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("Code.DEFAULT", "Code.PAY", "Code.SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldMergeDefaultMethodWithConcreteOverrides() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default String getCode() { return "DEFAULT"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public String getCode() { return "PAY"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public String getCode() { return "SHIP"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
null, null, false, null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("DEFAULT", "PAY", "SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldPreferConcreteCicOverInterfaceDefault() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { DEFAULT, PAY, SHIP }
|
||||
""");
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default Code getCode() { return Code.DEFAULT; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public Code getCode() { return Code.PAY; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
void run() {
|
||||
EventSource src = new PayEvent();
|
||||
}
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
org.eclipse.jdt.core.dom.CompilationUnit cu = (org.eclipse.jdt.core.dom.CompilationUnit) runner.getRoot();
|
||||
final org.eclipse.jdt.core.dom.ClassInstanceCreation[] payEventCic =
|
||||
new org.eclipse.jdt.core.dom.ClassInstanceCreation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.ClassInstanceCreation node) {
|
||||
if ("PayEvent".equals(node.getType().toString())) {
|
||||
payEventCic[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
payEventCic[0],
|
||||
cu,
|
||||
false,
|
||||
null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactly("Code.PAY");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class NonIndexedConcreteOverride {
|
||||
|
||||
@Test
|
||||
void dataFlowShouldResolveNonTrivialConcreteOverride() throws IOException {
|
||||
writeJava("com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
Payload payload = new CustomPayload();
|
||||
String event = payload.getEvent();
|
||||
}
|
||||
}
|
||||
interface Payload {
|
||||
String getEvent();
|
||||
}
|
||||
class CustomPayload implements Payload {
|
||||
public String getEvent() {
|
||||
return sideEffect();
|
||||
}
|
||||
private String sideEffect() { return "CUSTOM"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
MethodDeclaration run = context.findMethodDeclaration(runner, "run", true);
|
||||
final MethodInvocation[] getterCall = new MethodInvocation[1];
|
||||
run.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getInitializer() instanceof MethodInvocation mi) {
|
||||
getterCall[0] = mi;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
DataFlowModel model = new JdtDataFlowModel(context);
|
||||
String resolved = model.resolveValue(getterCall[0], context);
|
||||
assertThat(resolved).isEqualTo("CUSTOM");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GetterChainThroughInterface {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveDeepChainWithInterfaceMiddleHop() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
CentralDispatcher dispatcher;
|
||||
Outer outer;
|
||||
public void pay() {
|
||||
dispatcher.fire(outer.getInner().getPayload().getEvent());
|
||||
}
|
||||
}
|
||||
class CentralDispatcher {
|
||||
public void fire(OrderEvent event) {}
|
||||
}
|
||||
interface Inner {
|
||||
Payload getPayload();
|
||||
}
|
||||
class InnerImpl implements Inner {
|
||||
private Payload payload = new Payload();
|
||||
public Payload getPayload() { return payload; }
|
||||
}
|
||||
class Outer {
|
||||
public Inner getInner() { return new InnerImpl(); }
|
||||
}
|
||||
class Payload {
|
||||
private OrderEvent event = OrderEvent.PAY;
|
||||
public OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.CentralDispatcher")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.contains("OrderEvent.PAY");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConcreteSubtypeTrigger {
|
||||
|
||||
@Test
|
||||
void callGraphShouldPreferConcreteSubtypeFieldOverAbstractDefault() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
OrderService service;
|
||||
public void ship() {
|
||||
service.fire(new ShipEvent());
|
||||
}
|
||||
}
|
||||
class OrderService {
|
||||
StateMachine sm;
|
||||
public void fire(BaseEvent event) {
|
||||
sm.sendEvent(event.getCode());
|
||||
}
|
||||
}
|
||||
enum Code { PAY, SHIP }
|
||||
abstract class BaseEvent {
|
||||
protected Code code = Code.PAY;
|
||||
public Code getCode() { return code; }
|
||||
}
|
||||
class ShipEvent extends BaseEvent {
|
||||
public ShipEvent() { this.code = Code.SHIP; }
|
||||
}
|
||||
class StateMachine { void sendEvent(Code event) {} }
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("ship")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("Code.SHIP");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConcreteSubtypeTriggerViaFire {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveSubtypeWhenPassedToBaseTypedFire() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
OrderService service;
|
||||
public void ship() {
|
||||
service.fire(new ShipEvent());
|
||||
}
|
||||
}
|
||||
class OrderService {
|
||||
public void fire(BaseEvent event) {}
|
||||
}
|
||||
enum Code { PAY, SHIP }
|
||||
abstract class BaseEvent {
|
||||
protected Code code = Code.PAY;
|
||||
public Code getCode() { return code; }
|
||||
}
|
||||
class ShipEvent extends BaseEvent {
|
||||
public ShipEvent() { this.code = Code.SHIP; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("ship")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.OrderService")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getMethodChain())
|
||||
.containsExactly(
|
||||
"com.example.ApiController.ship",
|
||||
"com.example.OrderService.fire");
|
||||
assertThat(chains.get(0).getTriggerPoint().getEvent())
|
||||
.contains("ShipEvent");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class UnresolvedTypeWidening {
|
||||
|
||||
@Test
|
||||
void constantExtractorShouldWidenWhenTypeDeclarationMissing() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { A, B }
|
||||
""");
|
||||
writeJava("com/example/EventA.java", """
|
||||
package com.example;
|
||||
public class EventA implements RichEvent {
|
||||
public Code getCode() { return Code.A; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/EventB.java", """
|
||||
package com.example;
|
||||
public class EventB implements RichEvent {
|
||||
public Code getCode() { return Code.B; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/RichEvent.java", """
|
||||
package com.example;
|
||||
public interface RichEvent {
|
||||
Code getCode();
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = constantExtractor.resolveMethodReturnConstant(
|
||||
"RichEvent", "getCode", 0, new HashSet<>(), null);
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("Code.A", "Code.B");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DeterministicImplementations {
|
||||
|
||||
@Test
|
||||
void getImplementationsShouldReturnSortedStableOrder() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
String getCode();
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ZebraEvent.java", """
|
||||
package com.example;
|
||||
public class ZebraEvent implements EventSource {
|
||||
public String getCode() { return "Z"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/AlphaEvent.java", """
|
||||
package com.example;
|
||||
public class AlphaEvent implements EventSource {
|
||||
public String getCode() { return "A"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> first = context.getImplementations("com.example.EventSource");
|
||||
List<String> second = context.getImplementations("com.example.EventSource");
|
||||
|
||||
assertThat(first).containsExactly(
|
||||
"com.example.AlphaEvent",
|
||||
"com.example.ZebraEvent");
|
||||
assertThat(second).isEqualTo(first);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ReactiveFlatMapLambdaSendEvent {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterInsideFlatMapLambda() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload).flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
OrderEvent event = OrderEvent.PAY;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterInsideFlatMapBlockBody() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload).flatMap(p -> {
|
||||
sm.sendEvent(p.getEvent());
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
OrderEvent event = OrderEvent.PAY;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
static Mono empty() { return new Mono(); }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterThroughNestedFlatMap() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload)
|
||||
.flatMap(x -> Mono.just(x.getInner()))
|
||||
.flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
Inner inner = new Inner();
|
||||
Inner getInner() { return inner; }
|
||||
}
|
||||
class Inner {
|
||||
OrderEvent event = OrderEvent.SHIP;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.SHIP");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ReactiveExpressionSupportTest {
|
||||
|
||||
@Test
|
||||
void shouldMapLambdaParameterToOuterJustPayload(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
Payload payload;
|
||||
void run() {
|
||||
Mono.just(payload).flatMap(p -> Mono.just(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload { String getEvent() { return "X"; } }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
""";
|
||||
Path file = tempDir.resolve("Runner.java");
|
||||
Files.writeString(file, source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
CompilationUnit cu = (CompilationUnit) runner.getRoot();
|
||||
final MethodInvocation[] flatMapCall = new MethodInvocation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ("flatMap".equals(node.getName().getIdentifier()) && flatMapCall[0] == null) {
|
||||
flatMapCall[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
String payload = ReactiveExpressionSupport.extractPayload(
|
||||
flatMapCall[0], context.getConstantResolver(), context);
|
||||
assertThat(payload).isEqualTo("payload.getEvent()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemapLambdaParameterGetter(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
Payload payload;
|
||||
StateMachine sm;
|
||||
void run() {
|
||||
Mono.just(payload).flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload { String getEvent() { return "X"; } }
|
||||
class StateMachine { void sendEvent(String e) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
""";
|
||||
Path file = tempDir.resolve("Runner.java");
|
||||
Files.writeString(file, source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
CompilationUnit cu = (CompilationUnit) runner.getRoot();
|
||||
final MethodInvocation[] sendEventCall = new MethodInvocation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ("sendEvent".equals(node.getName().getIdentifier()) && sendEventCall[0] == null) {
|
||||
sendEventCall[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
Expression eventExpr = (Expression) sendEventCall[0].arguments().get(0);
|
||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||
eventExpr, context.getConstantResolver(), context);
|
||||
assertThat(remapped).isEqualTo("payload.getEvent()");
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ class AnalysisCacheCorrectnessTest {
|
||||
List<CallChain> first = intelligence.findCallChains(entryPoints, triggers);
|
||||
List<CallChain> second = intelligence.findCallChains(entryPoints, triggers);
|
||||
|
||||
assertThat(second).isSameAs(first);
|
||||
assertThat(second).isEqualTo(first);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* <p>Each test embeds a miniature codebase as a string so humans and agents can see the full
|
||||
* REST → dispatcher → sendEvent pipeline without opening another module.
|
||||
*
|
||||
* <p>DTO setter/getter propagation is covered by {@code HeuristicCallGraphEngineTypeTest}.
|
||||
* Cross-hop routing helpers that return computed strings are covered by literal-forwarding tests above.
|
||||
* <p>Cross-hop routing helpers and DTO constructor→getter propagation are covered by Tier 1 tests below.
|
||||
*/
|
||||
class CentralDispatcherResolutionTest {
|
||||
|
||||
@@ -418,6 +417,105 @@ class CentralDispatcherResolutionTest {
|
||||
"OrderEvent.PAY");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tier 1 — field-injected routing helper returns string literals consumed by central dispatcher.
|
||||
*
|
||||
* <pre>
|
||||
* pay() → gateway.routeAndPay() → dispatch("ORDER", routingHelper.payAction()) → OrderEvent.PAY
|
||||
* ship() → gateway.routeAndShip() → dispatch("ORDER", routingHelper.shipAction()) → OrderEvent.SHIP
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void fieldWiredRoutingHelperShouldForwardLiteralsToCentralDispatcher(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
CommandGateway gateway;
|
||||
public void pay() { gateway.routeAndPay(); }
|
||||
public void ship() { gateway.routeAndShip(); }
|
||||
}
|
||||
class CommandGateway {
|
||||
OrderRoutingHelper routingHelper;
|
||||
CentralDispatcher dispatcher;
|
||||
void routeAndPay() { dispatcher.dispatch("ORDER", routingHelper.payAction()); }
|
||||
void routeAndShip() { dispatcher.dispatch("ORDER", routingHelper.shipAction()); }
|
||||
}
|
||||
class OrderRoutingHelper {
|
||||
String payAction() { return "PAY"; }
|
||||
String shipAction() { return "SHIP"; }
|
||||
}
|
||||
class CentralDispatcher {
|
||||
void dispatch(String domain, String action) {
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(OrderEvent.valueOf(action));
|
||||
}
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
||||
class OrderStateMachineConfig {}
|
||||
""";
|
||||
CodebaseContext context = scanSource(source, tempDir);
|
||||
|
||||
CallChain payChain = resolveChain(context, CONTROLLER, "pay", STATE_MACHINE);
|
||||
CallChain shipChain = resolveChain(context, CONTROLLER, "ship", STATE_MACHINE);
|
||||
|
||||
assertPolyEvents(payChain, "OrderEvent.PAY");
|
||||
assertPolyEvents(shipChain, "OrderEvent.SHIP");
|
||||
|
||||
assertLinkedEvent(
|
||||
linkEndpointToTransition(payChain, MACHINE_CONFIG,
|
||||
transition("NEW", "PAID", "OrderEvent.PAY"),
|
||||
transition("PAID", "SHIPPED", "OrderEvent.SHIP")),
|
||||
"OrderEvent.PAY");
|
||||
assertLinkedEvent(
|
||||
linkEndpointToTransition(shipChain, MACHINE_CONFIG,
|
||||
transition("NEW", "PAID", "OrderEvent.PAY"),
|
||||
transition("PAID", "SHIPPED", "OrderEvent.SHIP")),
|
||||
"OrderEvent.SHIP");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tier 1 — DTO field set via constructor in controller, read via getter in gateway.
|
||||
*
|
||||
* <pre>
|
||||
* submit() → gateway.submit(new OrderRequest(OrderEvent.SUBMIT)) → req.getType() → OrderEvent.SUBMIT
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void dtoCrossHopFieldFlowShouldReachCentralDispatcher(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
CommandGateway gateway;
|
||||
public void submit() { gateway.submit(new OrderRequest(OrderEvent.SUBMIT)); }
|
||||
}
|
||||
class CommandGateway {
|
||||
void submit(OrderRequest req) {
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(req.getType());
|
||||
}
|
||||
}
|
||||
class OrderRequest {
|
||||
private final OrderEvent type;
|
||||
OrderRequest(OrderEvent type) { this.type = type; }
|
||||
OrderEvent getType() { return type; }
|
||||
}
|
||||
enum OrderEvent { SUBMIT, PAY }
|
||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
||||
class OrderStateMachineConfig {}
|
||||
""";
|
||||
CodebaseContext context = scanSource(source, tempDir);
|
||||
|
||||
CallChain submitChain = resolveChain(context, CONTROLLER, "submit", STATE_MACHINE);
|
||||
|
||||
assertPolyEvents(submitChain, "OrderEvent.SUBMIT");
|
||||
assertLinkedEvent(
|
||||
linkEndpointToTransition(submitChain, MACHINE_CONFIG,
|
||||
transition("DRAFT", "SUBMITTED", "OrderEvent.SUBMIT"),
|
||||
transition("SUBMITTED", "PAID", "OrderEvent.PAY")),
|
||||
"OrderEvent.SUBMIT");
|
||||
}
|
||||
|
||||
/**
|
||||
* Chained field-backed trivial getters ({@code return this.field}) through a central dispatcher.
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,11 @@ final class CentralDispatcherTestSupport {
|
||||
private CentralDispatcherTestSupport() {
|
||||
}
|
||||
|
||||
enum EngineKind {
|
||||
HEURISTIC,
|
||||
JDT
|
||||
}
|
||||
|
||||
static CodebaseContext scanSource(String source, Path tempDir) throws Exception {
|
||||
Files.writeString(tempDir.resolve("App.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
@@ -38,7 +43,18 @@ final class CentralDispatcherTestSupport {
|
||||
String controllerClass,
|
||||
String controllerMethod,
|
||||
String stateMachineClass) {
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
return resolveChain(context, controllerClass, controllerMethod, stateMachineClass, EngineKind.HEURISTIC);
|
||||
}
|
||||
|
||||
static CallChain resolveChain(
|
||||
CodebaseContext context,
|
||||
String controllerClass,
|
||||
String controllerMethod,
|
||||
String stateMachineClass,
|
||||
EngineKind engineKind) {
|
||||
AbstractCallGraphEngine engine = engineKind == EngineKind.JDT
|
||||
? new JdtCallGraphEngine(context, null)
|
||||
: new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className(controllerClass)
|
||||
.methodName(controllerMethod)
|
||||
@@ -50,7 +66,8 @@ final class CentralDispatcherTestSupport {
|
||||
.build();
|
||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||
assertThat(chains)
|
||||
.as("expected one call chain from %s.%s to %s.sendEvent", controllerClass, controllerMethod, stateMachineClass)
|
||||
.as("expected one call chain from %s.%s to %s.sendEvent via %s",
|
||||
controllerClass, controllerMethod, stateMachineClass, engineKind)
|
||||
.hasSize(1);
|
||||
return chains.get(0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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 org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.EngineKind;
|
||||
import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.assertPolyEvents;
|
||||
import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.scanSource;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomMessageTriggerResolutionTest {
|
||||
|
||||
private static final String SOURCE = """
|
||||
package com.example;
|
||||
enum OrderEvent { PROCESS, COMPLETE }
|
||||
class CustomStateMachineMessage<T, P> {
|
||||
CustomStateMachineMessage(T payload, P customPayload) {}
|
||||
}
|
||||
class StateMachineService {
|
||||
void sendCustomMessage(CustomStateMachineMessage<OrderEvent, String> customMessage) {}
|
||||
}
|
||||
class RabbitOrderListener {
|
||||
private StateMachineService stateMachineService;
|
||||
void handleCustomTransition(String payload) {
|
||||
CustomStateMachineMessage<OrderEvent, String> customMessage =
|
||||
new CustomStateMachineMessage<>(OrderEvent.PROCESS, "payload: " + payload);
|
||||
stateMachineService.sendCustomMessage(customMessage);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(EngineKind.class)
|
||||
void customStateMachineMessageShouldExtractEventFromConstructor(EngineKind engineKind, @TempDir Path tempDir)
|
||||
throws Exception {
|
||||
var context = scanSource(SOURCE, tempDir);
|
||||
AbstractCallGraphEngine engine = engineKind == EngineKind.JDT
|
||||
? new JdtCallGraphEngine(context, null)
|
||||
: new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.RabbitOrderListener")
|
||||
.methodName("handleCustomTransition")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachineService")
|
||||
.methodName("sendCustomMessage")
|
||||
.event("customMessage")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
assertPolyEvents(chains.get(0), "OrderEvent.PROCESS");
|
||||
}
|
||||
}
|
||||
@@ -1037,6 +1037,42 @@ class HeuristicCallGraphEngineCoreTest {
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("REACTIVE_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEventFromNestedReactiveFlatMap(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderController {
|
||||
private StateMachine stateMachine;
|
||||
public void handleReactive() {
|
||||
Mono.just("IGNORED")
|
||||
.flatMap(x -> Mono.just("NESTED_REACTIVE_EVENT"))
|
||||
.flatMap(stateMachine::sendEvent);
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
public void sendEvent(String event) {}
|
||||
}
|
||||
|
||||
class Mono {
|
||||
public static Mono just(Object obj) { return new Mono(); }
|
||||
public Mono flatMap(Object mapper) { return this; }
|
||||
}
|
||||
""";
|
||||
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("handleReactive").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));
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder("NESTED_REACTIVE_EVENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractEventFromSwitchExpressionAndParentheses(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
||||
import click.kamil.springstatemachineexporter.service.ExportService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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;
|
||||
|
||||
/**
|
||||
* End-to-end verification for {@code state_machines/inheritance_sample}:
|
||||
* interface-mapped REST → injected service impl → {@code sendEvent}.
|
||||
*/
|
||||
class InheritanceSampleChainTest {
|
||||
|
||||
private static Path findProjectRoot() {
|
||||
Path current = Path.of(".").toAbsolutePath();
|
||||
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
|
||||
current = current.getParent();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportShouldIncludeTriggerAndCallChain(@TempDir Path tempDir) throws Exception {
|
||||
Path sampleRoot = findProjectRoot().resolve("state_machines/inheritance_sample");
|
||||
|
||||
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||
exportService.runExporter(sampleRoot, tempDir, List.of("json"), true, List.of(), null, null,
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Path jsonFile;
|
||||
try (var stream = Files.walk(tempDir)) {
|
||||
jsonFile = stream.filter(p -> p.getFileName().toString().contains("InheritanceStateMachineConfig")
|
||||
&& p.getFileName().toString().endsWith(".json")).findFirst().orElseThrow();
|
||||
}
|
||||
JsonNode metadata = mapper.readTree(jsonFile.toFile()).path("metadata");
|
||||
|
||||
assertThat(metadata.path("triggers").size()).as("triggers").isGreaterThan(0);
|
||||
assertThat(metadata.path("callChains").size()).as("callChains").isGreaterThan(0);
|
||||
|
||||
JsonNode chain = metadata.path("callChains").get(0);
|
||||
assertThat(chain.path("entryPoint").path("name").asText())
|
||||
.isEqualTo("POST /api/v2/orders/submit");
|
||||
assertThat(chain.path("methodChain").toString())
|
||||
.contains("OrderControllerImpl.submitOrder")
|
||||
.contains("ProcessingServiceImpl.doProcess");
|
||||
assertThat(chain.path("triggerPoint").path("event").asText())
|
||||
.isIn("INHERITED_SUBMIT", "java.lang.String.INHERITED_SUBMIT", "String.INHERITED_SUBMIT");
|
||||
assertThat(chain.path("matchedTransitions").size()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.EngineKind;
|
||||
import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Ensures the production {@link JdtCallGraphEngine} resolves single-dispatcher endpoints the same
|
||||
* way as {@link HeuristicCallGraphEngine} for Tier-1 central-dispatcher patterns.
|
||||
*/
|
||||
class JdtCentralDispatcherParityTest {
|
||||
|
||||
private static final String CONTROLLER = "com.example.ApiController";
|
||||
private static final String STATE_MACHINE = "com.example.StateMachine";
|
||||
private static final String MACHINE_CONFIG = "com.example.OrderStateMachineConfig";
|
||||
|
||||
@Test
|
||||
void jdtShouldMatchHeuristicForFieldWiredRoutingHelper(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
CommandGateway gateway;
|
||||
public void pay() { gateway.routeAndPay(); }
|
||||
public void ship() { gateway.routeAndShip(); }
|
||||
}
|
||||
class CommandGateway {
|
||||
OrderRoutingHelper routingHelper;
|
||||
CentralDispatcher dispatcher;
|
||||
void routeAndPay() { dispatcher.dispatch("ORDER", routingHelper.payAction()); }
|
||||
void routeAndShip() { dispatcher.dispatch("ORDER", routingHelper.shipAction()); }
|
||||
}
|
||||
class OrderRoutingHelper {
|
||||
String payAction() { return "PAY"; }
|
||||
String shipAction() { return "SHIP"; }
|
||||
}
|
||||
class CentralDispatcher {
|
||||
void dispatch(String domain, String action) {
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(OrderEvent.valueOf(action));
|
||||
}
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
||||
class OrderStateMachineConfig {}
|
||||
""";
|
||||
CodebaseContext context = scanSource(source, tempDir);
|
||||
|
||||
CallChain heuristicPay = resolveChain(context, CONTROLLER, "pay", STATE_MACHINE, EngineKind.HEURISTIC);
|
||||
CallChain jdtPay = resolveChain(context, CONTROLLER, "pay", STATE_MACHINE, EngineKind.JDT);
|
||||
CallChain heuristicShip = resolveChain(context, CONTROLLER, "ship", STATE_MACHINE, EngineKind.HEURISTIC);
|
||||
CallChain jdtShip = resolveChain(context, CONTROLLER, "ship", STATE_MACHINE, EngineKind.JDT);
|
||||
|
||||
assertThat(jdtPay.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrderElementsOf(heuristicPay.getTriggerPoint().getPolymorphicEvents());
|
||||
assertThat(jdtShip.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrderElementsOf(heuristicShip.getTriggerPoint().getPolymorphicEvents());
|
||||
|
||||
MatchedTransition heuristicLink = linkEndpointToTransition(heuristicPay, MACHINE_CONFIG,
|
||||
transition("NEW", "PAID", "OrderEvent.PAY"),
|
||||
transition("PAID", "SHIPPED", "OrderEvent.SHIP"));
|
||||
MatchedTransition jdtLink = linkEndpointToTransition(jdtPay, MACHINE_CONFIG,
|
||||
transition("NEW", "PAID", "OrderEvent.PAY"),
|
||||
transition("PAID", "SHIPPED", "OrderEvent.SHIP"));
|
||||
assertThat(jdtLink.getEvent()).isEqualTo(heuristicLink.getEvent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jdtShouldMatchHeuristicForDtoCrossHop(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
CommandGateway gateway;
|
||||
public void submit() { gateway.submit(new OrderRequest(OrderEvent.SUBMIT)); }
|
||||
}
|
||||
class CommandGateway {
|
||||
void submit(OrderRequest req) {
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(req.getType());
|
||||
}
|
||||
}
|
||||
class OrderRequest {
|
||||
private final OrderEvent type;
|
||||
OrderRequest(OrderEvent type) { this.type = type; }
|
||||
OrderEvent getType() { return type; }
|
||||
}
|
||||
enum OrderEvent { SUBMIT, PAY }
|
||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
||||
class OrderStateMachineConfig {}
|
||||
""";
|
||||
CodebaseContext context = scanSource(source, tempDir);
|
||||
|
||||
CallChain heuristic = resolveChain(context, CONTROLLER, "submit", STATE_MACHINE, EngineKind.HEURISTIC);
|
||||
CallChain jdt = resolveChain(context, CONTROLLER, "submit", STATE_MACHINE, EngineKind.JDT);
|
||||
|
||||
assertThat(jdt.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrderElementsOf(heuristic.getTriggerPoint().getPolymorphicEvents());
|
||||
|
||||
MatchedTransition heuristicLink = linkEndpointToTransition(heuristic, MACHINE_CONFIG,
|
||||
transition("DRAFT", "SUBMITTED", "OrderEvent.SUBMIT"),
|
||||
transition("SUBMITTED", "PAID", "OrderEvent.PAY"));
|
||||
MatchedTransition jdtLink = linkEndpointToTransition(jdt, MACHINE_CONFIG,
|
||||
transition("DRAFT", "SUBMITTED", "OrderEvent.SUBMIT"),
|
||||
transition("SUBMITTED", "PAID", "OrderEvent.PAY"));
|
||||
assertThat(jdtLink.getEvent()).isEqualTo(heuristicLink.getEvent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jdtShouldMatchHeuristicForSingleDispatcherNamedMethods(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
CentralDispatcher dispatcher;
|
||||
public void pay() { dispatcher.orderPay(); }
|
||||
public void ship() { dispatcher.orderShip(); }
|
||||
}
|
||||
class CentralDispatcher {
|
||||
public void orderPay() {
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(OrderEvent.PAY);
|
||||
}
|
||||
public void orderShip() {
|
||||
StateMachine sm = new StateMachine();
|
||||
sm.sendEvent(OrderEvent.SHIP);
|
||||
}
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
||||
class OrderStateMachineConfig {}
|
||||
""";
|
||||
CodebaseContext context = scanSource(source, tempDir);
|
||||
|
||||
for (String method : List.of("pay", "ship")) {
|
||||
CallChain heuristic = resolveChain(context, CONTROLLER, method, STATE_MACHINE, EngineKind.HEURISTIC);
|
||||
CallChain jdt = resolveChain(context, CONTROLLER, method, STATE_MACHINE, EngineKind.JDT);
|
||||
assertThat(jdt.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrderElementsOf(heuristic.getTriggerPoint().getPolymorphicEvents());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
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.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Guards map-routed endpoint resolution and per-machine polymorphic event isolation when the
|
||||
* call graph and accessor caches are reused across multiple {@link HeuristicCallGraphEngine#findChains}
|
||||
* invocations on the same {@link CodebaseContext}.
|
||||
*/
|
||||
class PerformanceCacheMultiMachineIsolationTest {
|
||||
|
||||
private static final String ORDER_CONTROLLER = "com.example.OrderController";
|
||||
private static final String SHIPMENT_CONTROLLER = "com.example.ShipmentController";
|
||||
private static final String ORDER_MACHINE = "com.example.OrderMachine";
|
||||
private static final String SHIPMENT_MACHINE = "com.example.ShipmentMachine";
|
||||
|
||||
@Test
|
||||
void mapRoutedEndpointsShouldResolveSingleEventPerLiteralEndpoint(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.Map;
|
||||
public class OrderController {
|
||||
OrderMachine sm;
|
||||
public void payOrder() { sm.sendEvent(Routes.ROUTES.get("order.pay")); }
|
||||
public void shipOrder() { sm.sendEvent(Routes.ROUTES.get("order.ship")); }
|
||||
}
|
||||
class Routes {
|
||||
static final Map<String, OrderEvent> ROUTES = Map.of(
|
||||
"order.pay", OrderEvent.PAY,
|
||||
"order.ship", OrderEvent.SHIP
|
||||
);
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP, CANCEL }
|
||||
class OrderMachine { public void sendEvent(OrderEvent e) {} }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("App.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className(ORDER_MACHINE)
|
||||
.methodName("sendEvent")
|
||||
.event("e")
|
||||
.build();
|
||||
|
||||
EntryPoint payEntry = EntryPoint.builder().className(ORDER_CONTROLLER).methodName("payOrder").build();
|
||||
EntryPoint shipEntry = EntryPoint.builder().className(ORDER_CONTROLLER).methodName("shipOrder").build();
|
||||
|
||||
CallChain payChain = engine.findChains(List.of(payEntry), List.of(trigger)).get(0);
|
||||
CallChain shipChain = engine.findChains(List.of(shipEntry), List.of(trigger)).get(0);
|
||||
|
||||
assertThat(payChain.getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderEvent.PAY");
|
||||
assertThat(shipChain.getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderEvent.SHIP");
|
||||
|
||||
assertThat(context.getCache()).containsKey("heuristicCallGraph");
|
||||
|
||||
CallChain payAgain = engine.findChains(List.of(payEntry), List.of(trigger)).get(0);
|
||||
CallChain shipAgain = engine.findChains(List.of(shipEntry), List.of(trigger)).get(0);
|
||||
|
||||
assertThat(payAgain.getTriggerPoint().getPolymorphicEvents())
|
||||
.isEqualTo(payChain.getTriggerPoint().getPolymorphicEvents());
|
||||
assertThat(shipAgain.getTriggerPoint().getPolymorphicEvents())
|
||||
.isEqualTo(shipChain.getTriggerPoint().getPolymorphicEvents());
|
||||
}
|
||||
|
||||
@Test
|
||||
void separateMachinesShouldNotCrossContaminateMapRoutedResolution(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.Map;
|
||||
public class OrderController {
|
||||
OrderMachine orderMachine;
|
||||
public void pay() { orderMachine.sendEvent(OrderRoutes.ROUTES.get("order.pay")); }
|
||||
}
|
||||
public class ShipmentController {
|
||||
ShipmentMachine shipmentMachine;
|
||||
public void dispatch() { shipmentMachine.sendEvent(ShipmentRoutes.ROUTES.get("shipment.dispatch")); }
|
||||
}
|
||||
class OrderRoutes {
|
||||
static final Map<String, OrderEvent> ROUTES = Map.of("order.pay", OrderEvent.PAY);
|
||||
}
|
||||
class ShipmentRoutes {
|
||||
static final Map<String, ShipmentEvent> ROUTES = Map.of("shipment.dispatch", ShipmentEvent.DISPATCH);
|
||||
}
|
||||
enum OrderEvent { PAY }
|
||||
enum ShipmentEvent { DISPATCH }
|
||||
class OrderMachine { public void sendEvent(OrderEvent e) {} }
|
||||
class ShipmentMachine { public void sendEvent(ShipmentEvent e) {} }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("App.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint orderEntry = EntryPoint.builder().className(ORDER_CONTROLLER).methodName("pay").build();
|
||||
EntryPoint shipmentEntry = EntryPoint.builder().className(SHIPMENT_CONTROLLER).methodName("dispatch").build();
|
||||
TriggerPoint orderTrigger = TriggerPoint.builder()
|
||||
.className(ORDER_MACHINE)
|
||||
.methodName("sendEvent")
|
||||
.event("e")
|
||||
.build();
|
||||
TriggerPoint shipmentTrigger = TriggerPoint.builder()
|
||||
.className(SHIPMENT_MACHINE)
|
||||
.methodName("sendEvent")
|
||||
.event("e")
|
||||
.build();
|
||||
|
||||
engine.findChains(List.of(orderEntry), List.of(orderTrigger));
|
||||
CallChain shipmentChain = engine.findChains(List.of(shipmentEntry), List.of(shipmentTrigger)).get(0);
|
||||
assertThat(shipmentChain.getTriggerPoint().getPolymorphicEvents()).containsExactly("ShipmentEvent.DISPATCH");
|
||||
|
||||
CallChain orderChain = engine.findChains(List.of(orderEntry), List.of(orderTrigger)).get(0);
|
||||
assertThat(orderChain.getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderEvent.PAY");
|
||||
}
|
||||
}
|
||||
@@ -71,4 +71,40 @@ class SpringMvcDetectorTest {
|
||||
assertThat(entryPoints).anyMatch(ep ->
|
||||
"POST /api/orders/pay".equals(ep.getName()) && "pay".equals(ep.getMethodName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDetectInterfaceMappedRestEndpointOnImplOnly(@TempDir Path tempDir) throws Exception {
|
||||
String apiSource = """
|
||||
package com.example;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@RequestMapping("/api/v2/orders")
|
||||
public interface OrderApi {
|
||||
@PostMapping("/submit")
|
||||
void submitOrder();
|
||||
}
|
||||
""";
|
||||
String implSource = """
|
||||
package com.example;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@RestController
|
||||
public class OrderControllerImpl implements OrderApi {
|
||||
@Override
|
||||
public void submitOrder() {}
|
||||
}
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("OrderApi.java"), apiSource);
|
||||
Files.writeString(tempDir.resolve("OrderControllerImpl.java"), implSource);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.scan(tempDir);
|
||||
|
||||
SpringMvcDetector detector = new SpringMvcDetector(context, context.getConstantResolver());
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration impl = context.getTypeDeclaration("com.example.OrderControllerImpl");
|
||||
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) impl.getRoot());
|
||||
|
||||
assertThat(entryPoints).hasSize(1);
|
||||
assertThat(entryPoints.get(0).getName()).isEqualTo("POST /api/v2/orders/submit");
|
||||
assertThat(entryPoints.get(0).getClassName()).isEqualTo("com.example.OrderControllerImpl");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TypeResolverTest {
|
||||
|
||||
@Test
|
||||
void shouldMapGenericEventPlaceholderToSingleParameterIndex(@TempDir Path tempDir) throws Exception {
|
||||
Files.writeString(tempDir.resolve("Machine.java"), """
|
||||
package com.example;
|
||||
class StateMachine {
|
||||
void sendEvent(OrderEvent event) {}
|
||||
}
|
||||
enum OrderEvent { PAY }
|
||||
""");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
TypeResolver resolver = new TypeResolver(context);
|
||||
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "event")).isZero();
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "e", true)).isZero();
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "e")).isEqualTo(-1);
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "payload")).isEqualTo(-1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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 FindLocalSetterArgumentTest {
|
||||
|
||||
private CodebaseContext context;
|
||||
private VariableTracer variableTracer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/Payload.java", """
|
||||
package com.example;
|
||||
public class Payload {
|
||||
private String event = "DEFAULT";
|
||||
public String getEvent() { return event; }
|
||||
public void setEvent(String event) { this.event = event; }
|
||||
}
|
||||
""");
|
||||
context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||
variableTracer = new VariableTracer(context, context.getConstantResolver());
|
||||
}
|
||||
|
||||
@Test
|
||||
void lastSetterBeforeGetterWins(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
Payload payload = new Payload();
|
||||
payload.setEvent("FIRST");
|
||||
payload.setEvent("SECOND");
|
||||
String value = payload.getEvent();
|
||||
}
|
||||
}
|
||||
""");
|
||||
context.scan(tempDir);
|
||||
|
||||
Expression result = variableTracer.traceLocalSetter(
|
||||
"com.example.Runner.run", "payload", "getEvent");
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("SECOND");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setterAfterGetterIsIgnored(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
Payload payload = new Payload();
|
||||
payload.setEvent("BEFORE");
|
||||
String value = payload.getEvent();
|
||||
payload.setEvent("AFTER");
|
||||
}
|
||||
}
|
||||
""");
|
||||
context.scan(tempDir);
|
||||
|
||||
Expression result = variableTracer.traceLocalSetter(
|
||||
"com.example.Runner.run", "payload", "getEvent");
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("BEFORE");
|
||||
assertThat(result.toString()).doesNotContain("AFTER");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ifElseBranchesBothReachGetter_lastOnCfgPathWins(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run(boolean flag) {
|
||||
Payload payload = new Payload();
|
||||
if (flag) {
|
||||
payload.setEvent("IF_BRANCH");
|
||||
} else {
|
||||
payload.setEvent("ELSE_BRANCH");
|
||||
}
|
||||
String value = payload.getEvent();
|
||||
}
|
||||
}
|
||||
""");
|
||||
context.scan(tempDir);
|
||||
|
||||
Expression result = variableTracer.traceLocalSetter(
|
||||
"com.example.Runner.run", "payload", "getEvent");
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
// Both branches are on the CFG path to the getter; last in source order wins.
|
||||
assertThat(result.toString()).contains("ELSE_BRANCH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setterOnSiblingBranchIsExcluded(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run(boolean flag) {
|
||||
Payload payload = new Payload();
|
||||
if (flag) {
|
||||
payload.setEvent("TAKEN");
|
||||
String value = payload.getEvent();
|
||||
return;
|
||||
}
|
||||
payload.setEvent("OTHER_BRANCH");
|
||||
}
|
||||
}
|
||||
""");
|
||||
context.scan(tempDir);
|
||||
|
||||
Expression result = variableTracer.traceLocalSetter(
|
||||
"com.example.Runner.run", "payload", "getEvent");
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("TAKEN");
|
||||
assertThat(result.toString()).doesNotContain("OTHER_BRANCH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noGetterCallFallsBackToOrderedScan(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
Payload payload = new Payload();
|
||||
payload.setEvent("ONLY");
|
||||
}
|
||||
}
|
||||
""");
|
||||
context.scan(tempDir);
|
||||
|
||||
Expression result = variableTracer.traceLocalSetter(
|
||||
"com.example.Runner.run", "payload", "getEvent");
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("ONLY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldAssignmentSetterIsFound(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/MutablePayload.java", """
|
||||
package com.example;
|
||||
public class MutablePayload {
|
||||
public String event;
|
||||
public String getEvent() { return event; }
|
||||
}
|
||||
""");
|
||||
writeJava(tempDir, "com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
MutablePayload payload = new MutablePayload();
|
||||
payload.event = "ASSIGNED";
|
||||
String value = payload.getEvent();
|
||||
}
|
||||
}
|
||||
""");
|
||||
context.scan(tempDir);
|
||||
|
||||
Expression result = variableTracer.traceLocalSetter(
|
||||
"com.example.Runner.run", "payload", "getEvent");
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("ASSIGNED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setterInsideNestedExpressionIsFound(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
Payload payload = new Payload();
|
||||
consume(payload.setEvent("NESTED"));
|
||||
String value = payload.getEvent();
|
||||
}
|
||||
private void consume(String ignored) {}
|
||||
}
|
||||
""");
|
||||
context.scan(tempDir);
|
||||
|
||||
Expression result = variableTracer.traceLocalSetter(
|
||||
"com.example.Runner.run", "payload", "getEvent");
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.toString()).contains("NESTED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNullWhenNoMatchingSetter(@TempDir Path tempDir) throws IOException {
|
||||
writeJava(tempDir, "com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
Payload payload = new Payload();
|
||||
String value = payload.getEvent();
|
||||
}
|
||||
}
|
||||
""");
|
||||
context.scan(tempDir);
|
||||
|
||||
Expression result = variableTracer.traceLocalSetter(
|
||||
"com.example.Runner.run", "payload", "getEvent");
|
||||
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, source);
|
||||
}
|
||||
}
|
||||
@@ -147,120 +147,6 @@
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/submit",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.submitDocument", "click.kamil.enterprise.web.StateMachineDispatcher.submitDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "submitDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 44,
|
||||
"polymorphicEvents" : [ "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "click.kamil.enterprise.machines.document.DocumentState.DRAFT",
|
||||
"targetState" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW",
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.SUBMIT"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/approve",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/approve",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.approveDocument", "click.kamil.enterprise.web.StateMachineDispatcher.approveDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "approveDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 51,
|
||||
"polymorphicEvents" : [ "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW",
|
||||
"targetState" : "click.kamil.enterprise.machines.document.DocumentState.APPROVED",
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.APPROVE"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/document/reject",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/document/reject",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.rejectDocument", "click.kamil.enterprise.web.StateMachineDispatcher.rejectDocument" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "rejectDocument",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 58,
|
||||
"polymorphicEvents" : [ "click.kamil.enterprise.machines.document.DocumentEvent.REJECT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "click.kamil.enterprise.machines.document.DocumentState.IN_REVIEW",
|
||||
"targetState" : "click.kamil.enterprise.machines.document.DocumentState.DRAFT",
|
||||
"event" : "click.kamil.enterprise.machines.document.DocumentEvent.REJECT"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
|
||||
@@ -182,8 +182,112 @@
|
||||
"stateTypeFqn" : "String",
|
||||
"eventTypeFqn" : "String",
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"triggers" : [ {
|
||||
"event" : "java.lang.String.AUDIT_EVENT",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
"methodName" : "preHandle",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/SecurityInterceptor.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
}, {
|
||||
"event" : "java.lang.String.PLACE_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
"methodName" : "place",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/OrderServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
}, {
|
||||
"event" : "java.lang.String.CANCEL_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
"methodName" : "cancel",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/OrderServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
}, {
|
||||
"event" : "java.lang.String.PAY_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
||||
"methodName" : "processPayment",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/ReactivePaymentService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
}, {
|
||||
"event" : "java.lang.String.SHIP_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||
"methodName" : "onShippingReady",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ShippingJmsListener.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
}, {
|
||||
"event" : "java.lang.String.RETURN_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||
"methodName" : "onReturn",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ReturnsRabbitListener.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/orders/place",
|
||||
"className" : "click.kamil.examples.enterprise.api.OrderController",
|
||||
"methodName" : "placeOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/orders/place",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/orders/{id}/cancel",
|
||||
"className" : "click.kamil.examples.enterprise.api.OrderController",
|
||||
"methodName" : "cancelOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/orders/{id}/cancel",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "id",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "CUSTOM",
|
||||
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
@@ -239,7 +343,222 @@
|
||||
"annotations" : [ ]
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/orders/place",
|
||||
"className" : "click.kamil.examples.enterprise.api.OrderController",
|
||||
"methodName" : "placeOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/orders/place",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.placeOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.place" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.PLACE_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
"methodName" : "place",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/OrderServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : [ "java.lang.String.PLACE_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.NEW",
|
||||
"targetState" : "String.CHECK_AVAILABILITY",
|
||||
"event" : "String.PLACE_ORDER"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/orders/{id}/cancel",
|
||||
"className" : "click.kamil.examples.enterprise.api.OrderController",
|
||||
"methodName" : "cancelOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/orders/{id}/cancel",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "id",
|
||||
"type" : "String",
|
||||
"annotations" : [ "PathVariable" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.cancelOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.cancel" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.CANCEL_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.OrderServiceImpl",
|
||||
"methodName" : "cancel",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/OrderServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : [ "java.lang.String.CANCEL_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.PAID",
|
||||
"targetState" : "String.CANCELLED",
|
||||
"event" : "String.CANCEL_ORDER"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "CUSTOM",
|
||||
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
"methodName" : "preHandle",
|
||||
"sourceFile" : null,
|
||||
"metadata" : {
|
||||
"interceptorType" : "Spring MVC Interceptor"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.SecurityInterceptor.preHandle" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.AUDIT_EVENT",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
"methodName" : "preHandle",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/SecurityInterceptor.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/enterprise/payments",
|
||||
"className" : "click.kamil.examples.enterprise.api.ReactivePaymentController",
|
||||
"methodName" : "pay",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/ReactivePaymentController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/enterprise/payments",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.ReactivePaymentController.pay", "click.kamil.examples.enterprise.service.ReactivePaymentService.processPayment" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.PAY_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.service.ReactivePaymentService",
|
||||
"methodName" : "processPayment",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/service/ReactivePaymentService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : [ "java.lang.String.PAY_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.PENDING_PAYMENT",
|
||||
"targetState" : "String.PAID",
|
||||
"event" : "String.PAY_ORDER"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "JMS",
|
||||
"name" : "JMS: shipping.queue",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||
"methodName" : "onShippingReady",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ShippingJmsListener.java",
|
||||
"metadata" : {
|
||||
"protocol" : "JMS",
|
||||
"destination" : "shipping.queue"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ShippingJmsListener.onShippingReady" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.SHIP_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ShippingJmsListener",
|
||||
"methodName" : "onShippingReady",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ShippingJmsListener.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.PAID",
|
||||
"targetState" : "String.SHIPPED",
|
||||
"event" : "String.SHIP_ORDER"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "RABBIT",
|
||||
"name" : "RABBIT: returns.queue",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||
"methodName" : "onReturn",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ReturnsRabbitListener.java",
|
||||
"metadata" : {
|
||||
"protocol" : "RABBIT",
|
||||
"destination" : "returns.queue"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener.onReturn" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.RETURN_ORDER",
|
||||
"className" : "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener",
|
||||
"methodName" : "onReturn",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/messaging/ReturnsRabbitListener.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.DELIVERED",
|
||||
"targetState" : "String.RETURNED",
|
||||
"event" : "String.RETURN_ORDER"
|
||||
} ]
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
|
||||
@@ -182,40 +182,6 @@
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/resume",
|
||||
"className" : "click.kamil.examples.statemachine.extended.web.OrderController",
|
||||
"methodName" : "resumeOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/resume",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.resumeOrder", "click.kamil.examples.statemachine.extended.service.OrderService.resumeOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "[LIFECYCLE:RESTORE]",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
"methodName" : "resumeOrder",
|
||||
"sourceFile" : null,
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 29,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : "orderId",
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/payment/{id}/capture",
|
||||
|
||||
@@ -182,40 +182,6 @@
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/resume",
|
||||
"className" : "click.kamil.examples.statemachine.extended.web.OrderController",
|
||||
"methodName" : "resumeOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/resume",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.resumeOrder", "click.kamil.examples.statemachine.extended.service.OrderService.resumeOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "[LIFECYCLE:RESTORE]",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.OrderService",
|
||||
"methodName" : "resumeOrder",
|
||||
"sourceFile" : null,
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 29,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : "orderId",
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/payment/{id}/capture",
|
||||
|
||||
@@ -32,9 +32,67 @@
|
||||
"stateTypeFqn" : "String",
|
||||
"eventTypeFqn" : "String",
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"triggers" : [ {
|
||||
"event" : "java.lang.String.INHERITED_SUBMIT",
|
||||
"className" : "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl",
|
||||
"methodName" : "doProcess",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/inheritance/service/ProcessingServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 15,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/v2/orders/submit",
|
||||
"className" : "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/inheritance/api/OrderControllerImpl.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/v2/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/v2/orders/submit",
|
||||
"className" : "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/inheritance/api/OrderControllerImpl.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/v2/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl.submitOrder", "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl.doProcess" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.INHERITED_SUBMIT",
|
||||
"className" : "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl",
|
||||
"methodName" : "doProcess",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/inheritance/service/ProcessingServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 15,
|
||||
"polymorphicEvents" : [ "java.lang.String.INHERITED_SUBMIT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.START",
|
||||
"targetState" : "String.WORKING",
|
||||
"event" : "String.INHERITED_SUBMIT"
|
||||
} ]
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
|
||||
@@ -60,8 +60,61 @@
|
||||
"eventTypeFqn" : "String",
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ ],
|
||||
"callChains" : [ ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /maven/orders/submit",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
|
||||
"metadata" : {
|
||||
"path" : "/maven/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /maven/orders/submit",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
|
||||
"metadata" : {
|
||||
"path" : "/maven/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.maven.core.MavenOrderStateMachine.OrderController.submitOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.SUBMIT",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 40,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.INIT",
|
||||
"targetState" : "String.BUSY",
|
||||
"event" : "String.SUBMIT"
|
||||
} ]
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>State Machine Explorer - click.kamil.maven.core.MavenOrderStateMachine</title>
|
||||
<!-- Popper and Tippy for tooltips -->
|
||||
<script src="https://unpkg.com/@popperjs/core@2"></script>
|
||||
<script src="https://unpkg.com/tippy.js@6"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/animations/scale.css"/>
|
||||
<!-- SVG Pan & Zoom -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-bg: #ffffff;
|
||||
--main-bg: #f8fafc;
|
||||
--accent: #ef4444;
|
||||
--primary: #3b82f6;
|
||||
--text: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
background: var(--main-bg);
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 480px;
|
||||
min-width: 350px;
|
||||
max-width: 900px;
|
||||
background: var(--sidebar-bg);
|
||||
border-right: 2px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 10px 0 15px rgba(0,0,0,0.02);
|
||||
z-index: 10;
|
||||
overflow-x: hidden;
|
||||
resize: horizontal;
|
||||
position: relative;
|
||||
transition: width 0.3s ease, min-width 0.3s ease, padding 0.3s ease;
|
||||
}
|
||||
|
||||
#sidebar.collapsed {
|
||||
width: 0 !important;
|
||||
min-width: 0 !important;
|
||||
border-right: none;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
#toggle-sidebar {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 50;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
#toggle-sidebar:hover {
|
||||
background: #f8fafc;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
#toggle-sidebar svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: var(--text-muted);
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
#sidebar::after {
|
||||
content: '⋮';
|
||||
position: absolute;
|
||||
right: 2px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--border);
|
||||
font-size: 1.2rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 30px 30px 10px;
|
||||
}
|
||||
|
||||
header h1 { font-weight: 900; font-size: 1.6rem; color: var(--text); margin: 0 0 5px 0; letter-spacing: -0.5px; }
|
||||
header p { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-muted); margin: 0 0 15px 0; opacity: 0.7; }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
padding: 0 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 0;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { color: var(--primary); }
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px; left: 0; right: 0;
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex-grow: 1;
|
||||
padding: 25px 30px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
.ep-card, .flow-card {
|
||||
padding: 18px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
margin-bottom: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
position: relative;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ep-card:hover, .flow-card:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 15px 30px -10px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.ep-type {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 900;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
.type-rest { background: #eff6ff; color: #1d4ed8; }
|
||||
.type-custom { background: #fef2f2; color: #b91c1c; }
|
||||
.type-jms { background: #f5f3ff; color: #7c3aed; }
|
||||
.type-sqs { background: #fff7ed; color: #ea580c; }
|
||||
.type-sns { background: #fff1f2; color: #e11d48; }
|
||||
.type-kafka { background: #fefce8; color: #854d0e; }
|
||||
.type-rabbit { background: #f0fdf4; color: #166534; }
|
||||
|
||||
.ep-name, .flow-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 6px; color: var(--text); }
|
||||
.ep-fqn, .flow-desc { font-family: 'Inter', sans-serif; font-size: 0.75rem; color: var(--text-muted); opacity: 0.9; line-height: 1.4; }
|
||||
|
||||
#main { flex-grow: 1; position: relative; background: var(--main-bg); overflow: hidden; }
|
||||
#svg-container {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* SVG Interactivity Styles */
|
||||
svg a { text-decoration: none !important; }
|
||||
|
||||
.active-path {
|
||||
stroke: var(--accent) !important;
|
||||
stroke-width: 2.5px !important;
|
||||
filter: drop-shadow(0 0 8px rgba(239, 68, 68, 0.3));
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.active-path text, a.active-path text {
|
||||
fill: #000 !important;
|
||||
font-weight: 900 !important;
|
||||
paint-order: stroke;
|
||||
stroke: #fff;
|
||||
stroke-width: 3.5px;
|
||||
}
|
||||
|
||||
.dimmed {
|
||||
opacity: 0.2 !important;
|
||||
filter: grayscale(1);
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Tippy Styling */
|
||||
.tippy-box[data-theme~='modern'] {
|
||||
background-color: #fff;
|
||||
color: var(--text);
|
||||
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 0;
|
||||
max-width: 550px !important;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.payload-box {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.payload-param {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.payload-param:last-child { margin-bottom: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<header>
|
||||
<h1>Explorer</h1>
|
||||
<p>click.kamil.maven.core.MavenOrderStateMachine</p>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="explorer">Explorer</div>
|
||||
<div class="tab" data-tab="flows">Business Flows</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-explorer" class="tab-content active">
|
||||
<div id="ep-list"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-flows" class="tab-content">
|
||||
<div id="flow-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<button id="toggle-sidebar" aria-label="Toggle Sidebar" title="Toggle Sidebar">
|
||||
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
</button>
|
||||
<div id="svg-container">
|
||||
<?xml version="1.0" encoding="us-ascii" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="100%" preserveAspectRatio="xMidYMid meet" version="1.1" viewBox="0 0 187 255" width="100%" zoomAndPan="magnify"><defs/><g><ellipse cx="35.5208" cy="34.375" fill="#222222" rx="11.4583" ry="11.4583" style="stroke:#222222;stroke-width:1.1458333333333333;"/><rect fill="#FFFFFF" height="45.8333" rx="14.3229" ry="14.3229" style="stroke:#94A3B8;stroke-width:1.1458333333333333;" width="57.2917" x="6.875" y="101.9792"/><text fill="#000000" font-family="Inter" font-size="10.3125" font-weight="bold" lengthAdjust="spacing" textLength="21.7708" x="24.6354" y="128.7932">INIT</text><rect fill="#FFFFFF" height="45.8333" rx="14.3229" ry="14.3229" style="stroke:#94A3B8;stroke-width:1.1458333333333333;" width="57.2917" x="6.875" y="202.8125"/><text fill="#000000" font-family="Inter" font-size="10.3125" font-weight="bold" lengthAdjust="spacing" textLength="27.5" x="21.7708" y="229.6266">BUSY</text><rect fill="#FFFFFF" height="45.8333" rx="14.3229" ry="14.3229" style="stroke:#94A3B8;stroke-width:1.1458333333333333;" width="57.2917" x="68.75" y="13.75"/><text fill="#000000" font-family="Inter" font-size="10.3125" font-weight="bold" lengthAdjust="spacing" textLength="29.7917" x="82.5" y="40.5641">DONE</text><ellipse cx="97.3958" cy="123.75" fill="none" rx="12.6042" ry="12.6042" style="stroke:#222222;stroke-width:1.1458333333333333;"/><ellipse cx="97.3958" cy="123.75" fill="#222222" rx="6.875" ry="6.875" style="stroke:#222222;stroke-width:1.1458333333333333;"/><polygon fill="#CBD5E1" points="35.5208,101.6748,40.1042,91.3623,35.5208,95.9456,30.9375,91.3623,35.5208,101.6748" style="stroke:#CBD5E1;stroke-width:1.1458333333333333;"/><path d="M35.5208,50.4929 C35.5208,63.9394 35.5208,78.5626 35.5208,94.7998 " fill="none" style="stroke:#CBD5E1;stroke-width:1.1458333333333333;"/><polygon fill="#1E90FF" points="35.5208,202.7749,40.1042,192.4624,35.5208,197.0457,30.9375,192.4624,35.5208,202.7749" style="stroke:#1E90FF;stroke-width:1.1458333333333333;"/><path d="M35.5208,147.9113 C35.5208,164.2413 35.5208,179.5846 35.5208,195.8999 " fill="none" style="stroke:#1E90FF;stroke-width:2.2916666666666665;"/><a href="#link_ORDER_EVENT" target="_self" title="#link_ORDER_EVENT" xlink:actuate="onRequest" xlink:href="#link_ORDER_EVENT" xlink:show="new" xlink:title="#link_ORDER_EVENT" xlink:type="simple"><text fill="#0000FF" font-family="JetBrains Mono" font-size="9.1667" lengthAdjust="spacing" text-decoration="underline" textLength="142.0833" x="36.6667" y="178.4456">ORDER_EVENT / loggingAction()</text></a><polygon fill="#CBD5E1" points="97.3958,110.7767,101.9792,100.4642,97.3958,105.0475,92.8125,100.4642,97.3958,110.7767" style="stroke:#CBD5E1;stroke-width:1.1458333333333333;"/><path d="M97.3958,59.9435 C97.3958,76.0566 97.3958,90.453 97.3958,103.9017 " fill="none" style="stroke:#CBD5E1;stroke-width:1.1458333333333333;"/><!--SRC=[RP5lJy8m4CRVzrESuOqQYSm_2HX20Z8IZ881D364a6CzjeQkNTeYJkDtjmF4XVYgrz-rzpntTv8PZ5C4YRbUEx0fELJ8BFcOCZJej06b5R54S09ACvS39niPaJcXrGvRHuQqopDYTYLKyI_r41t15mFeOBIAZLuhVg-bhxT9XAE2QyF9x5YbSOFNY_g1JX8HhHHP2u5dFQtS05E2ll9IUp0MdmIDtulB9S52I-x1QATb51cugddmZ9mB5VjQtsM72NAzAVWIfIrxRnkZDmVH1t8TWq9PUD9A__TiQwL-dDbt5YtuBGN7oNA3VocU2GY2MjdaU_mer6g29lPBcLkIIyQcvpEeLblG7_GdZB7YWEgq4eIDMgztKKnXvhETb_4RD9lquMUcKBPQnMK-77N3qJny3GSJJ-vWEgr8Br3cK8ulGUeuzbDgHyN6JyzcCyQwmq6uTU2T_000]--></g></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="metadata-json" type="application/json">
|
||||
{
|
||||
"name" : "click.kamil.maven.core.MavenOrderStateMachine",
|
||||
"states" : [ {
|
||||
"rawName" : "DONE",
|
||||
"fullIdentifier" : "DONE"
|
||||
}, {
|
||||
"rawName" : "\"INIT\"",
|
||||
"fullIdentifier" : "INIT"
|
||||
}, {
|
||||
"rawName" : "\"BUSY\"",
|
||||
"fullIdentifier" : "BUSY"
|
||||
}, {
|
||||
"rawName" : "INIT",
|
||||
"fullIdentifier" : "INIT"
|
||||
} ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"INIT\"",
|
||||
"fullIdentifier" : "INIT"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"BUSY\"",
|
||||
"fullIdentifier" : "BUSY"
|
||||
} ],
|
||||
"event" : "ORDER_EVENT",
|
||||
"guard" : null,
|
||||
"actions" : [ {
|
||||
"expression" : "loggingAction()",
|
||||
"isLambda" : false,
|
||||
"internalLogic" : "context -> {\n System.out.println(\"LOG: State transition in progress...\");\n}\n",
|
||||
"lineNumber" : 23
|
||||
} ],
|
||||
"order" : null
|
||||
} ],
|
||||
"startStates" : [ "INIT" ],
|
||||
"endStates" : [ "DONE" ],
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"flows" : [ ],
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "ORDER_EVENT",
|
||||
"className" : "click.kamil.maven.core.OrderService",
|
||||
"methodName" : "onMessage",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 34
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /reactive/order",
|
||||
"className" : "click.kamil.maven.api.ReactiveOrderApi",
|
||||
"methodName" : "orderRoutes",
|
||||
"metadata" : {
|
||||
"path" : "/reactive/order",
|
||||
"verb" : "POST",
|
||||
"style" : "WebFlux Functional"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "JMS",
|
||||
"name" : "JMS: order.queue",
|
||||
"className" : "click.kamil.maven.api.JmsOrderListener",
|
||||
"methodName" : "onMessage",
|
||||
"metadata" : {
|
||||
"protocol" : "JMS",
|
||||
"destination" : "order.queue"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "message",
|
||||
"type" : "String",
|
||||
"annotations" : [ ]
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const metadata = JSON.parse(document.getElementById('metadata-json').textContent);
|
||||
let panZoomInstance;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const svg = document.querySelector('#svg-container svg');
|
||||
panZoomInstance = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true });
|
||||
|
||||
document.getElementById('toggle-sidebar').addEventListener('click', () => {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
sidebar.classList.toggle('collapsed');
|
||||
|
||||
// Re-center SVG pan-zoom after transition
|
||||
setTimeout(() => {
|
||||
if (panZoomInstance) {
|
||||
panZoomInstance.resize();
|
||||
panZoomInstance.center();
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
setupTabs();
|
||||
populateSidebar();
|
||||
populateFlows();
|
||||
attachInteractivity();
|
||||
});
|
||||
|
||||
function setupTabs() {
|
||||
const flows = metadata.flows || [];
|
||||
if (flows.length === 0) {
|
||||
document.querySelector('.tabs').style.display = 'none';
|
||||
document.querySelector('.tab-content[data-tab="flows"]')?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab, .tab-content').forEach(el => el.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function populateSidebar() {
|
||||
const list = document.getElementById('ep-list');
|
||||
const entryPoints = metadata.metadata.entryPoints || [];
|
||||
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
|
||||
|
||||
entryPoints.forEach(ep => {
|
||||
// Quality Filter: Hide if it doesn't trigger a machine event
|
||||
const chains = (metadata.metadata.callChains || []).filter(c =>
|
||||
c.entryPoint.className === ep.className &&
|
||||
c.entryPoint.methodName === ep.methodName &&
|
||||
machineEvents.has(c.triggerPoint.event)
|
||||
);
|
||||
|
||||
if (chains.length === 0) return;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'ep-card';
|
||||
card.innerHTML = `
|
||||
<div class="ep-type type-${ep.type.toLowerCase()}">${ep.type}</div>
|
||||
<div class="ep-name">${ep.name}</div>
|
||||
<div class="ep-fqn">${ep.className}.${ep.methodName}</div>
|
||||
`;
|
||||
card.onmouseenter = () => highlightEntryPoint(ep);
|
||||
card.onmouseleave = clearHighlights;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function populateFlows() {
|
||||
const list = document.getElementById('flow-list');
|
||||
const flows = metadata.flows || [];
|
||||
|
||||
if (flows.length === 0) {
|
||||
list.innerHTML = '<p style="color:var(--text-muted); font-size:0.8rem; font-style:italic;">No business flows defined.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
flows.forEach(flow => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'flow-card';
|
||||
card.innerHTML = `
|
||||
<div class="flow-name">${flow.name}</div>
|
||||
<div class="flow-desc">${flow.description}</div>
|
||||
`;
|
||||
card.onmouseenter = () => highlightFlow(flow);
|
||||
card.onmouseleave = clearHighlights;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function highlightEntryPoint(ep) {
|
||||
clearHighlights();
|
||||
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
|
||||
const chains = metadata.metadata.callChains || [];
|
||||
const relevantEvents = chains
|
||||
.filter(c => c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName && machineEvents.has(c.triggerPoint.event))
|
||||
.map(c => c.triggerPoint.event);
|
||||
|
||||
if (relevantEvents.length === 0) return;
|
||||
highlightEvents(relevantEvents);
|
||||
}
|
||||
|
||||
function highlightFlow(flow) {
|
||||
clearHighlights();
|
||||
if (!flow.steps || flow.steps.length === 0) return;
|
||||
highlightEvents(flow.steps);
|
||||
}
|
||||
|
||||
function highlightEvents(steps) {
|
||||
const svg = document.querySelector('#svg-container svg');
|
||||
|
||||
// Dim everything first
|
||||
svg.querySelectorAll('path, rect, circle, ellipse, text, polygon').forEach(el => {
|
||||
el.classList.add('dimmed');
|
||||
});
|
||||
|
||||
steps.forEach(step => {
|
||||
let linkId = "";
|
||||
if (step.includes("->")) {
|
||||
// Named Choice Branch: "SRC->TGT"
|
||||
const parts = step.split("->");
|
||||
linkId = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]);
|
||||
} else {
|
||||
// Standard Event
|
||||
linkId = "#link_" + normalize(step);
|
||||
}
|
||||
|
||||
svg.querySelectorAll('a').forEach(link => {
|
||||
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
|
||||
if (href === linkId) {
|
||||
highlightLinkGroup(link);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Un-dim all state nodes for context
|
||||
svg.querySelectorAll('rect, circle, ellipse, polygon').forEach(el => {
|
||||
const isChoice = el.tagName === 'polygon' && el.points?.numberOfItems === 4;
|
||||
if (isChoice || ['rect', 'circle', 'ellipse'].includes(el.tagName)) {
|
||||
el.classList.remove('dimmed');
|
||||
let next = el.nextElementSibling;
|
||||
while(next && next.tagName === 'text') {
|
||||
next.classList.remove('dimmed');
|
||||
next = next.nextElementSibling;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function highlightLinkGroup(link) {
|
||||
link.classList.add('active-path');
|
||||
link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed'));
|
||||
|
||||
// Find path and polygon immediately before the <a> tag (interleaved)
|
||||
let prev = link.previousElementSibling;
|
||||
let foundPath = false;
|
||||
let foundPolygon = false;
|
||||
while (prev) {
|
||||
if (prev.tagName === 'path' && !foundPath) {
|
||||
prev.classList.remove('dimmed');
|
||||
prev.classList.add('active-path');
|
||||
foundPath = true;
|
||||
} else if (prev.tagName === 'polygon' && !foundPolygon && prev.points?.numberOfItems !== 4) {
|
||||
prev.classList.remove('dimmed');
|
||||
prev.classList.add('active-path');
|
||||
foundPolygon = true;
|
||||
} else if (['rect', 'circle', 'ellipse'].includes(prev.tagName) ||
|
||||
(prev.tagName === 'polygon' && prev.points?.numberOfItems === 4)) {
|
||||
// Hit a state node, stop traversal
|
||||
break;
|
||||
}
|
||||
if (foundPath && foundPolygon) break;
|
||||
prev = prev.previousElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
function clearHighlights() {
|
||||
document.querySelectorAll('.dimmed, .active-path').forEach(el => {
|
||||
el.classList.remove('dimmed', 'active-path');
|
||||
});
|
||||
}
|
||||
|
||||
function attachInteractivity() {
|
||||
const svg = document.querySelector('#svg-container svg');
|
||||
const transitions = metadata.transitions || [];
|
||||
|
||||
svg.querySelectorAll('a').forEach(link => {
|
||||
const href = link.getAttribute('xlink:href') || link.getAttribute('href');
|
||||
if (!href || !href.startsWith('#link_')) return;
|
||||
|
||||
const linkId = href.replace('#link_', '');
|
||||
const isAnon = linkId.startsWith('anon_');
|
||||
|
||||
let metaTrans = null;
|
||||
if (isAnon) {
|
||||
metaTrans = transitions.find(t =>
|
||||
!t.event &&
|
||||
"anon_" + normalize(t.sourceStates[0].fullIdentifier) + "_" + normalize(t.targetStates[0].fullIdentifier) === linkId
|
||||
);
|
||||
} else {
|
||||
metaTrans = transitions.find(t => normalize(t.event) === linkId);
|
||||
}
|
||||
|
||||
if (metaTrans || isAnon) {
|
||||
link.style.cursor = 'pointer';
|
||||
const setupTippy = (el) => {
|
||||
tippy(el, {
|
||||
content: createTooltip(isAnon ? "Internal logic" : linkId, metaTrans),
|
||||
allowHTML: true,
|
||||
theme: 'modern',
|
||||
interactive: true,
|
||||
appendTo: document.body,
|
||||
placement: 'right',
|
||||
offset: [0, 20]
|
||||
});
|
||||
};
|
||||
setupTippy(link);
|
||||
let path = link.previousElementSibling;
|
||||
while(path && path.tagName !== 'path') path = path.previousElementSibling;
|
||||
if (path) {
|
||||
path.style.cursor = 'pointer';
|
||||
setupTippy(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function normalize(s) {
|
||||
if (!s) return "";
|
||||
return s.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
}
|
||||
|
||||
function createTooltip(name, trans) {
|
||||
const chains = (metadata.metadata.callChains || []).filter(c => normalize(c.triggerPoint.event) === name);
|
||||
|
||||
let html = `<div class="tooltip-content">
|
||||
<div style="font-weight:900; font-size:1.1rem; margin-bottom:12px; border-bottom:1px solid #eee; padding-bottom:8px; line-height:1.3;">
|
||||
${name === 'Internal logic' ? name : 'Event: <span style="color:var(--accent)">' + name + '</span>'}
|
||||
</div>`;
|
||||
|
||||
if (trans && trans.guard) {
|
||||
const lineInfo = trans.guard.lineNumber ? ` <span style="color:#94a3b8; font-weight:400; font-size:0.6rem;">(Line: ${trans.guard.lineNumber})</span>` : '';
|
||||
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Guard${lineInfo}</div>
|
||||
<div style="background:#f1f5f9; padding:6px; border-radius:4px; font-family:monospace; font-size:0.8rem; margin-bottom:12px; border:1px solid #e2e8f0;">${trans.guard.expression}</div>`;
|
||||
if (trans.guard.internalLogic) {
|
||||
html += `<div style="font-size:0.65rem; font-weight:800; color:#cbd5e1; text-transform:uppercase; margin-bottom:4px">Internal Logic</div>
|
||||
<pre style="background:#1e293b; color:#f8fafc; padding:8px; border-radius:4px; font-family:'JetBrains Mono', monospace; font-size:0.7rem; overflow-x:auto; margin-bottom:12px;"><code>${trans.guard.internalLogic.replace(/</g, '<').replace(/>/g, '>')}</code></pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (trans && trans.actions && trans.actions.length > 0) {
|
||||
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Actions</div>`;
|
||||
trans.actions.forEach(action => {
|
||||
const lineInfo = action.lineNumber ? ` <span style="color:#94a3b8; font-weight:400; font-size:0.6rem;">(Line: ${action.lineNumber})</span>` : '';
|
||||
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:4px">Action${lineInfo}</div>
|
||||
<div style="background:#f1f5f9; padding:6px; border-radius:4px; font-family:monospace; font-size:0.8rem; margin-bottom:12px; border:1px solid #e2e8f0;">${action.expression}</div>`;
|
||||
if (action.internalLogic) {
|
||||
html += `<div style="font-size:0.65rem; font-weight:800; color:#cbd5e1; text-transform:uppercase; margin-bottom:4px">Internal Logic</div>
|
||||
<pre style="background:#1e293b; color:#f8fafc; padding:8px; border-radius:4px; font-family:'JetBrains Mono', monospace; font-size:0.7rem; overflow-x:auto; margin-bottom:12px;"><code>${action.internalLogic.replace(/</g, '<').replace(/>/g, '>')}</code></pre>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (chains && chains.length > 0) {
|
||||
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:10px; border-top:1px solid #eee; padding-top:10px">Triggered by</div>`;
|
||||
chains.forEach(c => {
|
||||
html += `<div style="margin-bottom:20px">
|
||||
<div style="font-weight:700; font-size:0.85rem; color:var(--text);">${c.entryPoint.name}</div>
|
||||
${renderParameters(c.entryPoint.parameters)}
|
||||
<div style="margin-left:10px; border-left:2px solid #e2e8f0; padding-left:12px; margin-top:10px">
|
||||
${c.methodChain.map((m, i) => `<div style="font-size:0.7rem; color:#475569; margin-top:4px; font-family:'JetBrains Mono', monospace;"><b>${i+1}</b> ${m}</div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderParameters(params) {
|
||||
if (!params || params.length === 0) return "";
|
||||
let html = `<div style="font-size:0.65rem; font-weight:700; color:#94a3b8; margin-top:8px; margin-bottom:4px; text-transform:uppercase;">Input Data</div>`;
|
||||
html += `<div class="payload-box">`;
|
||||
params.forEach(p => {
|
||||
const annotation = (p.annotations || []).find(a => a.endsWith("RequestBody") || a.endsWith("PathVariable") || a.endsWith("RequestParam"));
|
||||
const cleanAnn = annotation ? "@" + annotation.substring(annotation.lastIndexOf('.') + 1) : "";
|
||||
html += `<div class="payload-param">
|
||||
<span style="color:#f43f5e; font-size:0.65rem; font-weight:bold;">${cleanAnn}</span>
|
||||
<span style="color:#38bdf8;">${p.type}</span>
|
||||
<span style="color:#a3e635;">${p.name}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "ORDER_EVENT",
|
||||
"className" : "click.kamil.maven.core.OrderService",
|
||||
"methodName" : "onMessage",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 34
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /reactive/order",
|
||||
"className" : "click.kamil.maven.api.ReactiveOrderApi",
|
||||
"methodName" : "orderRoutes",
|
||||
"metadata" : {
|
||||
"path" : "/reactive/order",
|
||||
"verb" : "POST",
|
||||
"style" : "WebFlux Functional"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "JMS",
|
||||
"name" : "JMS: order.queue",
|
||||
"className" : "click.kamil.maven.api.JmsOrderListener",
|
||||
"methodName" : "onMessage",
|
||||
"metadata" : {
|
||||
"protocol" : "JMS",
|
||||
"destination" : "order.queue"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "message",
|
||||
"type" : "String",
|
||||
"annotations" : [ ]
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.maven.core.MavenOrderStateMachine",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "INIT" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"INIT\"",
|
||||
"fullIdentifier" : "INIT"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"BUSY\"",
|
||||
"fullIdentifier" : "BUSY"
|
||||
} ],
|
||||
"event" : "SUBMIT",
|
||||
"guard" : null,
|
||||
"actions" : [ {
|
||||
"expression" : "loggingAction()",
|
||||
"isLambda" : false,
|
||||
"internalLogic" : "context -> {\n System.out.println(\"LOG: State transition in progress...\");\n}\n",
|
||||
"lineNumber" : 23
|
||||
} ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"BUSY\"",
|
||||
"fullIdentifier" : "BUSY"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"DONE\"",
|
||||
"fullIdentifier" : "DONE"
|
||||
} ],
|
||||
"event" : "ORDER_EVENT",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"endStates" : [ "DONE" ]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
@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
|
||||
|
||||
[*] --> INIT
|
||||
|
||||
|
||||
INIT -[#1E90FF,bold]-> BUSY <<external>> <<e_ORDER_EVENT>> : ORDER_EVENT / loggingAction()
|
||||
|
||||
DONE --> [*]
|
||||
@enduml
|
||||
|
||||
@@ -59,7 +59,20 @@
|
||||
"stateTypeFqn" : "String",
|
||||
"eventTypeFqn" : "String",
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"triggers" : [ {
|
||||
"event" : "java.lang.String.SUBMIT",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/multi/core/OrderController.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /orders/submit",
|
||||
@@ -75,23 +88,46 @@
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /orders/submit",
|
||||
"className" : "click.kamil.multi.api.OrderApi",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "api-module/src/main/java/click/kamil/multi/api/OrderApi.java",
|
||||
"metadata" : {
|
||||
"path" : "/orders/submit",
|
||||
"verb" : "POST"
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /orders/submit",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/multi/core/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
"methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.SUBMIT",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/multi/core/OrderController.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.NEW",
|
||||
"targetState" : "String.PROCESSING",
|
||||
"event" : "String.SUBMIT"
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"metadata" : {
|
||||
"triggers" : [ {
|
||||
"event" : "SUBMIT",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 18
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /orders/submit",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"metadata" : {
|
||||
"path" : "/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /orders/submit",
|
||||
"className" : "click.kamil.multi.api.OrderApi",
|
||||
"methodName" : "submitOrder",
|
||||
"metadata" : {
|
||||
"path" : "/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /orders/submit",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"metadata" : {
|
||||
"path" : "/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "SUBMIT",
|
||||
"className" : "click.kamil.multi.core.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"lineNumber" : 18
|
||||
}
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
}
|
||||
},
|
||||
"name" : "click.kamil.multi.core.OrderStateMachineConfig",
|
||||
"renderChoicesAsDiamonds" : true,
|
||||
"startStates" : [ "NEW" ],
|
||||
"transitions" : [ {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"NEW\"",
|
||||
"fullIdentifier" : "NEW"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"PROCESSING\"",
|
||||
"fullIdentifier" : "PROCESSING"
|
||||
} ],
|
||||
"event" : "SUBMIT",
|
||||
"guard" : null,
|
||||
"actions" : [ {
|
||||
"expression" : "processAction()",
|
||||
"isLambda" : false,
|
||||
"internalLogic" : "@Override default void execute(StateContext<String,String> context){\n System.out.println(\"Processing order: \" + context.getMessageHeader(\"orderId\"));\n}\n",
|
||||
"lineNumber" : 31
|
||||
} ],
|
||||
"order" : null
|
||||
}, {
|
||||
"type" : "EXTERNAL",
|
||||
"sourceStates" : [ {
|
||||
"rawName" : "\"PROCESSING\"",
|
||||
"fullIdentifier" : "PROCESSING"
|
||||
} ],
|
||||
"targetStates" : [ {
|
||||
"rawName" : "\"COMPLETED\"",
|
||||
"fullIdentifier" : "COMPLETED"
|
||||
} ],
|
||||
"event" : "FINISH",
|
||||
"guard" : null,
|
||||
"actions" : [ ],
|
||||
"order" : null
|
||||
} ],
|
||||
"endStates" : [ "COMPLETED" ]
|
||||
}
|
||||
@@ -124,82 +124,6 @@
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/pay",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.payOrder", "click.kamil.enterprise.web.StateMachineDispatcher.payOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.PAY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "payOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 30,
|
||||
"polymorphicEvents" : [ "click.kamil.enterprise.machines.order.OrderEvent.PAY" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "click.kamil.enterprise.machines.order.OrderState.NEW",
|
||||
"targetState" : "click.kamil.enterprise.machines.order.OrderState.PENDING",
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.PAY"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/order/ship",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/order/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.shipOrder", "click.kamil.enterprise.web.StateMachineDispatcher.shipOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.SHIP",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "shipOrder",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 37,
|
||||
"polymorphicEvents" : [ "click.kamil.enterprise.machines.order.OrderEvent.SHIP" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "click.kamil.enterprise.machines.order.OrderState.PENDING",
|
||||
"targetState" : "click.kamil.enterprise.machines.order.OrderState.SHIPPED",
|
||||
"event" : "click.kamil.enterprise.machines.order.OrderEvent.SHIP"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
|
||||
@@ -87,6 +87,39 @@
|
||||
"ambiguous" : false
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/documents/submit",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.DocumentController",
|
||||
"methodName" : "submit",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/DocumentController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/documents/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/documents/approve",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.DocumentController",
|
||||
"methodName" : "approve",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/DocumentController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/documents/approve",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/documents/reject",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.DocumentController",
|
||||
"methodName" : "reject",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/DocumentController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/documents/reject",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/commands/{commandKey}",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
|
||||
|
||||
@@ -87,6 +87,83 @@
|
||||
"ambiguous" : false
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/pay",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.OrderController",
|
||||
"methodName" : "pay",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/ship",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.OrderController",
|
||||
"methodName" : "ship",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/cancel",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.OrderController",
|
||||
"methodName" : "cancel",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/cancel",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/string-dispatch/orders/pay",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
|
||||
"methodName" : "payWithStringKey",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/string-dispatch/orders/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/string-dispatch/orders/ship",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.StringDispatchController",
|
||||
"methodName" : "shipWithStringKey",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/StringDispatchController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/string-dispatch/orders/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/rich/pay",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.RichOrderController",
|
||||
"methodName" : "payViaRichEvent",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/RichOrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/rich/pay",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/rich/ship",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.RichOrderController",
|
||||
"methodName" : "shipViaRichEvent",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/layered/web/RichOrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/rich/ship",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/commands/{commandKey}",
|
||||
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
|
||||
|
||||
@@ -130,6 +130,50 @@
|
||||
"ambiguous" : false
|
||||
} ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/process",
|
||||
"className" : "click.kamil.web.OrderController",
|
||||
"methodName" : "processOrder",
|
||||
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/process",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/complete",
|
||||
"className" : "click.kamil.web.OrderController",
|
||||
"methodName" : "completeOrder",
|
||||
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/complete",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/cancel",
|
||||
"className" : "click.kamil.web.OrderController",
|
||||
"methodName" : "cancelOrder",
|
||||
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/cancel",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/orders/functional-complete",
|
||||
"className" : "click.kamil.web.OrderController",
|
||||
"methodName" : "functionalCompleteOrder",
|
||||
"sourceFile" : "web/src/main/java/click/kamil/web/OrderController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/orders/functional-complete",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ ]
|
||||
}, {
|
||||
"type" : "RABBIT",
|
||||
"name" : "RABBIT: order.custom.transition.queue",
|
||||
"className" : "click.kamil.web.RabbitOrderListener",
|
||||
@@ -342,13 +386,17 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 78,
|
||||
"polymorphicEvents" : [ ],
|
||||
"polymorphicEvents" : [ "click.kamil.domain.OrderEvent.PROCESS" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "click.kamil.domain.OrderState.NEW",
|
||||
"targetState" : "click.kamil.domain.OrderState.PROCESSING",
|
||||
"event" : "click.kamil.domain.OrderEvent.PROCESS"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "JMS",
|
||||
|
||||
@@ -117,78 +117,6 @@
|
||||
} ]
|
||||
} ],
|
||||
"callChains" : [ {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/verify",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/verify",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.verifyUser", "click.kamil.enterprise.web.StateMachineDispatcher.verifyUser" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "verifyUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 65,
|
||||
"polymorphicEvents" : [ "click.kamil.enterprise.machines.user.UserEvent.VERIFY" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "click.kamil.enterprise.machines.user.UserState.REGISTERED",
|
||||
"targetState" : "click.kamil.enterprise.machines.user.UserState.VERIFIED",
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.VERIFY"
|
||||
} ]
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/user/suspend",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineController",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineController.java",
|
||||
"metadata" : {
|
||||
"path" : "/api/machine/user/suspend",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "userId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestParam" ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.enterprise.web.StateMachineController.suspendUser", "click.kamil.enterprise.web.StateMachineDispatcher.suspendUser" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "click.kamil.enterprise.machines.user.UserEvent.SUSPEND",
|
||||
"className" : "click.kamil.enterprise.web.StateMachineDispatcher",
|
||||
"methodName" : "suspendUser",
|
||||
"sourceFile" : "src/main/java/click/kamil/enterprise/web/StateMachineDispatcher.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 72,
|
||||
"polymorphicEvents" : [ "click.kamil.enterprise.machines.user.UserEvent.SUSPEND" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "REST",
|
||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||
|
||||
Reference in New Issue
Block a user