Fix export metadata scoping and call-chain resolution gaps.
Correct machine-scope filtering for domain paths and Spring type erasure, skip phantom REST interfaces, resolve custom message wrapper events, scope JDT call-chain lookup per machine, and wire scan-time analysis indexes with JDT/heuristic parity tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -72,7 +72,12 @@ public final class MachineScopeFilter {
|
|||||||
String path = entryPoint.getMetadata() != null ? entryPoint.getMetadata().get("path") : null;
|
String path = entryPoint.getMetadata() != null ? entryPoint.getMetadata().get("path") : null;
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
String pathDomain = inferDomainFromPath(path);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (pathDomain == null && path.contains("{")) {
|
if (pathDomain == null && path.contains("{")) {
|
||||||
@@ -86,6 +91,11 @@ public final class MachineScopeFilter {
|
|||||||
return ROUTING.isRoutedToCorrectMachine(probe, machineName, context);
|
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) {
|
private static String inferDomainFromPath(String path) {
|
||||||
String lower = path.toLowerCase(Locale.ROOT);
|
String lower = path.toLowerCase(Locale.ROOT);
|
||||||
if (lower.contains("/orders") || lower.contains("order.")) {
|
if (lower.contains("/orders") || lower.contains("order.")) {
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
if (isTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
if (context != null && isSingleStateMachine(context)
|
||||||
|
&& (isErasedOrOpaqueType(triggerEventFqn) || isErasedOrOpaqueType(machineEventFqn))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (triggerStateFqn != null && machineStateFqn != null) {
|
if (triggerStateFqn != null && machineStateFqn != null) {
|
||||||
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
|
if (eraseGenerics(triggerStateFqn).equals(eraseGenerics(machineStateFqn))) {
|
||||||
@@ -37,6 +40,10 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
if (isTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
||||||
return false; // Mismatch!
|
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);
|
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) {
|
private boolean isSingleStateMachine(CodebaseContext context) {
|
||||||
return countStateMachines(context) <= 1;
|
return countStateMachines(context) <= 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.AccessorKind;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
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.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||||
@@ -46,6 +47,7 @@ public final class AccessorResolver {
|
|||||||
private final ConstantResolver constantResolver;
|
private final ConstantResolver constantResolver;
|
||||||
private final FieldInitializerFinder fieldInitializerFinder;
|
private final FieldInitializerFinder fieldInitializerFinder;
|
||||||
private final Map<String, List<String>> sessionCache = new HashMap<>();
|
private final Map<String, List<String>> sessionCache = new HashMap<>();
|
||||||
|
private final Map<String, Optional<AccessorSummary>> indexedGetterCache = new HashMap<>();
|
||||||
|
|
||||||
public AccessorResolver(
|
public AccessorResolver(
|
||||||
CodebaseContext context,
|
CodebaseContext context,
|
||||||
@@ -63,6 +65,16 @@ public final class AccessorResolver {
|
|||||||
if (className == null || className.isBlank()) {
|
if (className == null || className.isBlank()) {
|
||||||
return Optional.empty();
|
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);
|
Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
|
||||||
if (direct.isPresent()) {
|
if (direct.isPresent()) {
|
||||||
return direct;
|
return direct;
|
||||||
@@ -78,9 +90,14 @@ public final class AccessorResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<String> implementations = context.getImplementations(className);
|
List<String> implementations = context.getImplementations(className);
|
||||||
if (implementations == null) {
|
if (implementations == null || implementations.isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
Optional<AccessorSummary> precomputed = context.getPolymorphicAccessorIndex()
|
||||||
|
.representativeImplAccessor(className, methodName);
|
||||||
|
if (precomputed.isPresent()) {
|
||||||
|
return precomputed;
|
||||||
|
}
|
||||||
for (String implementation : implementations) {
|
for (String implementation : implementations) {
|
||||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
||||||
if (implAccessor.isPresent()) {
|
if (implAccessor.isPresent()) {
|
||||||
@@ -114,7 +131,7 @@ public final class AccessorResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> resolved = resolveInternal(ownerFqn, methodName, receiverContext, budget, visited);
|
List<String> resolved = resolveInternal(ownerFqn, methodName, receiverContext, budget, visited);
|
||||||
if (!resolved.isEmpty()) {
|
if (!resolved.isEmpty() || isStaticCacheable(receiverContext)) {
|
||||||
List<String> cachedCopy = List.copyOf(resolved);
|
List<String> cachedCopy = List.copyOf(resolved);
|
||||||
sessionCache.put(cacheKey, cachedCopy);
|
sessionCache.put(cacheKey, cachedCopy);
|
||||||
return cachedCopy;
|
return cachedCopy;
|
||||||
@@ -127,6 +144,12 @@ public final class AccessorResolver {
|
|||||||
return null;
|
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);
|
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, getterName);
|
||||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
||||||
AccessorSummary accessor = indexedAccessor.get();
|
AccessorSummary accessor = indexedAccessor.get();
|
||||||
@@ -165,8 +188,20 @@ public final class AccessorResolver {
|
|||||||
return null;
|
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() {
|
public void clearSessionCache() {
|
||||||
sessionCache.clear();
|
sessionCache.clear();
|
||||||
|
indexedGetterCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> resolveInternal(
|
private List<String> resolveInternal(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
protected final FieldInitializerFinder fieldInitializerFinder;
|
protected final FieldInitializerFinder fieldInitializerFinder;
|
||||||
protected final PathBindingEvaluator pathBindingEvaluator;
|
protected final PathBindingEvaluator pathBindingEvaluator;
|
||||||
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
||||||
|
private final Map<String, List<String>> polymorphicCallCache = new HashMap<>();
|
||||||
|
|
||||||
private ASTNode parseExpressionString(String expr) {
|
private ASTNode parseExpressionString(String expr) {
|
||||||
if (expr == null) return null;
|
if (expr == null) return null;
|
||||||
@@ -97,6 +98,13 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
|
accessorResolver.clearSessionCache();
|
||||||
|
constructorAnalyzer.clearReturnedCicCache();
|
||||||
|
variableTracer.clearAnalysisCaches();
|
||||||
|
polymorphicCallCache.clear();
|
||||||
|
context.clearAnalysisCaches();
|
||||||
|
pathFinder.clearAnalysisCaches();
|
||||||
|
parsedNodeCache.clear();
|
||||||
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
||||||
List<CallChain> chains = new ArrayList<>();
|
List<CallChain> chains = new ArrayList<>();
|
||||||
|
|
||||||
@@ -628,7 +636,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
} else if (exprNode instanceof ClassInstanceCreation cic) {
|
} else if (exprNode instanceof ClassInstanceCreation cic) {
|
||||||
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
sourceMethod = "inline-instantiation";
|
sourceMethod = "inline-instantiation";
|
||||||
|
extractEventConstantsFromConstructor(cic, polymorphicEvents);
|
||||||
|
if (polymorphicEvents.isEmpty()) {
|
||||||
polymorphicEvents.add(declaredType);
|
polymorphicEvents.add(declaredType);
|
||||||
|
}
|
||||||
} else if (exprNode instanceof ConditionalExpression cond) {
|
} else if (exprNode instanceof ConditionalExpression cond) {
|
||||||
List<Expression> branches = new ArrayList<>();
|
List<Expression> branches = new ArrayList<>();
|
||||||
branches.add(cond.getThenExpression());
|
branches.add(cond.getThenExpression());
|
||||||
@@ -703,13 +714,19 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
ce.getExpression() instanceof ClassInstanceCreation cic) {
|
ce.getExpression() instanceof ClassInstanceCreation cic) {
|
||||||
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
sourceMethod = "inline-instantiation";
|
sourceMethod = "inline-instantiation";
|
||||||
|
extractEventConstantsFromConstructor(cic, polymorphicEvents);
|
||||||
|
if (polymorphicEvents.isEmpty()) {
|
||||||
polymorphicEvents.add(declaredType);
|
polymorphicEvents.add(declaredType);
|
||||||
|
}
|
||||||
} else if (exprNode instanceof CastExpression ce &&
|
} else if (exprNode instanceof CastExpression ce &&
|
||||||
ce.getExpression() instanceof ClassInstanceCreation cic) {
|
ce.getExpression() instanceof ClassInstanceCreation cic) {
|
||||||
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||||
sourceMethod = "inline-instantiation";
|
sourceMethod = "inline-instantiation";
|
||||||
|
extractEventConstantsFromConstructor(cic, polymorphicEvents);
|
||||||
|
if (polymorphicEvents.isEmpty()) {
|
||||||
polymorphicEvents.add(declaredType);
|
polymorphicEvents.add(declaredType);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (methodName != null) {
|
if (methodName != null) {
|
||||||
if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) {
|
if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) {
|
||||||
@@ -887,7 +904,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
cuToUse = (CompilationUnit) entryTd.getRoot();
|
cuToUse = (CompilationUnit) entryTd.getRoot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<String> constants = constantExtractor.resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
|
List<String> constants = constantExtractor.resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse, ResolutionBudget.defaults(), false);
|
||||||
for (String constant : constants) {
|
for (String constant : constants) {
|
||||||
if (!polymorphicEvents.contains(constant)) {
|
if (!polymorphicEvents.contains(constant)) {
|
||||||
polymorphicEvents.add(constant);
|
polymorphicEvents.add(constant);
|
||||||
@@ -958,6 +975,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
qualifyBareEnumConstants(polymorphicEvents, expType);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> unpacked = new ArrayList<>();
|
List<String> unpacked = new ArrayList<>();
|
||||||
@@ -1127,9 +1145,365 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String postProcessResolvedArgument(Expression originalExpr, String resolvedValue) {
|
protected String postProcessResolvedArgument(Expression originalExpr, String resolvedValue) {
|
||||||
|
// Preserve valueOf(variable) so trigger tracing can bind the variable per call-path
|
||||||
|
// instead of widening to all enum constants at graph-build time.
|
||||||
|
if (originalExpr instanceof MethodInvocation valueOfMi
|
||||||
|
&& "valueOf".equals(valueOfMi.getName().getIdentifier())
|
||||||
|
&& valueOfMi.arguments().size() == 1
|
||||||
|
&& valueOfMi.arguments().get(0) instanceof SimpleName) {
|
||||||
|
return originalExpr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve field-wired helper calls (routingHelper.payAction()) for per-endpoint literal forwarding.
|
||||||
|
if (originalExpr instanceof MethodInvocation helperMi
|
||||||
|
&& helperMi.getExpression() instanceof SimpleName helperReceiver
|
||||||
|
&& !helperReceiver.getIdentifier().isEmpty()
|
||||||
|
&& Character.isLowerCase(helperReceiver.getIdentifier().charAt(0))
|
||||||
|
&& helperMi.arguments().isEmpty()
|
||||||
|
&& resolvedValue != null
|
||||||
|
&& resolvedValue.matches("^[A-Z_][A-Z0-9_]*$")) {
|
||||||
|
return originalExpr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 MethodInvocation mi
|
||||||
|
&& (mi.getExpression() instanceof ThisExpression || mi.getExpression() instanceof SuperMethodInvocation)
|
||||||
|
&& mi.arguments().isEmpty()) || (originalExpr instanceof SuperMethodInvocation smi && smi.arguments().isEmpty());
|
||||||
|
|
||||||
|
if (isThisOrSuperGetter) {
|
||||||
|
return originalExpr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized
|
||||||
|
// from a ClassInstanceCreation (directly or via a factory method).
|
||||||
|
if (originalExpr instanceof MethodInvocation getterMi
|
||||||
|
&& getterMi.getExpression() instanceof SimpleName instanceSn
|
||||||
|
&& getterMi.arguments().isEmpty()) {
|
||||||
|
String narrowed = tryNarrowGetterViaLocalVarChain(getterMi, instanceSn);
|
||||||
|
if (narrowed != null) {
|
||||||
|
return narrowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return resolvedValue;
|
return resolvedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 resolveReceiverTypeFallback(SimpleName receiverNameNode) {
|
||||||
|
String varName = receiverNameNode.getIdentifier();
|
||||||
|
|
||||||
|
CompilationUnit cu = receiverNameNode.getRoot() instanceof CompilationUnit rootCu ? rootCu : 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(receiverNameNode);
|
||||||
|
if (enclosingMethod != null) {
|
||||||
|
for (Object paramObj : enclosingMethod.parameters()) {
|
||||||
|
SingleVariableDeclaration svd = (SingleVariableDeclaration) paramObj;
|
||||||
|
if (svd.getName().getIdentifier().equals(varName)) {
|
||||||
|
return resolveTypeToFqn(svd.getType(), receiverNameNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object impObj : cu.imports()) {
|
||||||
|
ImportDeclaration imp = (ImportDeclaration) impObj;
|
||||||
|
String impName = imp.getName().getFullyQualifiedName();
|
||||||
|
if (impName.endsWith("." + simpleName)) {
|
||||||
|
return impName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return simpleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClassInstanceCreation cic = null;
|
||||||
|
String receiverType = null;
|
||||||
|
if (initExpr[0] instanceof ClassInstanceCreation directCic) {
|
||||||
|
cic = directCic;
|
||||||
|
} else if (initExpr[0] instanceof MethodInvocation factoryMi
|
||||||
|
&& (factoryMi.getExpression() instanceof SimpleName || factoryMi.getExpression() instanceof QualifiedName)) {
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> initialLocals = new 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> fieldValues = constructorAnalyzer.buildFieldValuesFromCIC(
|
||||||
|
varTypeTd, cic, context, this::evaluateMethodCallInConstructor, initialLocals);
|
||||||
|
if (fieldValues.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void trackSetterCallsBetween(
|
||||||
|
Block methodBody,
|
||||||
|
String varName,
|
||||||
|
MethodInvocation getterCall,
|
||||||
|
Map<String, String> fieldValues,
|
||||||
|
CodebaseContext context) {
|
||||||
|
List<ASTNode> statements = new ArrayList<>();
|
||||||
|
for (Object stmtObj : methodBody.statements()) {
|
||||||
|
statements.add((ASTNode) stmtObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
int varDeclIndex = -1;
|
||||||
|
int getterCallIndex = -1;
|
||||||
|
for (int i = 0; i < statements.size(); i++) {
|
||||||
|
ASTNode stmt = statements.get(i);
|
||||||
|
if (varDeclIndex == -1 && stmt instanceof VariableDeclarationStatement vds) {
|
||||||
|
for (Object fragObj : vds.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment frag
|
||||||
|
&& frag.getName().getIdentifier().equals(varName)) {
|
||||||
|
varDeclIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (getterCallIndex == -1 && stmt.toString().contains(getterCall.toString())) {
|
||||||
|
getterCallIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (varDeclIndex < 0 || getterCallIndex <= varDeclIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = varDeclIndex + 1; i < getterCallIndex; i++) {
|
||||||
|
ASTNode stmt = statements.get(i);
|
||||||
|
if (stmt instanceof ExpressionStatement es) {
|
||||||
|
Expression expr = es.getExpression();
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
if (mi.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
if (methodName.startsWith("set") && methodName.length() > 3 && !mi.arguments().isEmpty()) {
|
||||||
|
String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
|
||||||
|
String val = constantResolver.resolve((Expression) mi.arguments().get(0), context);
|
||||||
|
if (val != null) {
|
||||||
|
fieldValues.put(fieldName, val);
|
||||||
|
fieldValues.put("this." + fieldName, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (expr instanceof Assignment assignment) {
|
||||||
|
if (assignment.getLeftHandSide() instanceof FieldAccess fa) {
|
||||||
|
if (fa.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
|
||||||
|
String val = constantResolver.resolve(assignment.getRightHandSide(), context);
|
||||||
|
if (val != null) {
|
||||||
|
fieldValues.put(fa.getName().getIdentifier(), val);
|
||||||
|
fieldValues.put("this." + fa.getName().getIdentifier(), val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected String resolveMethodInType(TypeDeclaration td, String methodName) {
|
protected String resolveMethodInType(TypeDeclaration td, String methodName) {
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
if (md != null) {
|
if (md != null) {
|
||||||
@@ -1874,49 +2248,60 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
String baseCalled = resolveCalledMethod(node);
|
String baseCalled = resolveCalledMethod(node);
|
||||||
if (baseCalled == null) return Collections.emptyList();
|
if (baseCalled == null) return Collections.emptyList();
|
||||||
|
|
||||||
List<String> allResolved = new ArrayList<>();
|
|
||||||
|
|
||||||
if (node.getExpression() == null) {
|
if (node.getExpression() == null) {
|
||||||
return Collections.singletonList(baseCalled);
|
return Collections.singletonList(baseCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseCalled.contains(".")) {
|
if (!baseCalled.contains(".")) {
|
||||||
|
return Collections.singletonList(baseCalled);
|
||||||
|
}
|
||||||
|
|
||||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
boolean isImplicitThis = node.getExpression() instanceof ThisExpression;
|
||||||
|
String cacheKey = baseCalled + "#" + isImplicitThis;
|
||||||
|
List<String> cached = polymorphicCallCache.get(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> allResolved = new ArrayList<>();
|
||||||
List<String> impls = context.getImplementations(className);
|
List<String> impls = context.getImplementations(className);
|
||||||
|
|
||||||
boolean shouldExpand = true;
|
boolean shouldExpand = true;
|
||||||
if (impls.isEmpty()) {
|
if (impls.isEmpty()) {
|
||||||
shouldExpand = false;
|
shouldExpand = false;
|
||||||
} else if (td == null) {
|
} else if (td == null) {
|
||||||
// Do not polymorphically expand external generic types (e.g. java.lang.Runnable, Spring Action)
|
|
||||||
// across the entire codebase. This prevents massive false-positive tangling.
|
|
||||||
shouldExpand = false;
|
shouldExpand = false;
|
||||||
} else {
|
} else if (!td.isInterface() && !Modifier.isAbstract(td.getModifiers()) && isImplicitThis) {
|
||||||
boolean isImplicitThis = node.getExpression() instanceof ThisExpression;
|
|
||||||
if (!td.isInterface() && !Modifier.isAbstract(td.getModifiers()) && isImplicitThis) {
|
|
||||||
shouldExpand = false;
|
shouldExpand = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldExpand) {
|
if (shouldExpand) {
|
||||||
boolean isAbstractOrInterface = td.isInterface() || Modifier.isAbstract(td.getModifiers());
|
boolean isAbstractOrInterface = td.isInterface() || Modifier.isAbstract(td.getModifiers());
|
||||||
|
Optional<List<String>> precomputed =
|
||||||
|
context.getPolymorphicMethodExpansionIndex().lookup(className, methodName);
|
||||||
|
if (precomputed.isPresent()) {
|
||||||
|
if (!isAbstractOrInterface) {
|
||||||
|
allResolved.add(baseCalled);
|
||||||
|
}
|
||||||
|
allResolved.addAll(precomputed.get());
|
||||||
|
} else {
|
||||||
if (!isAbstractOrInterface) {
|
if (!isAbstractOrInterface) {
|
||||||
allResolved.add(baseCalled);
|
allResolved.add(baseCalled);
|
||||||
}
|
}
|
||||||
for (String impl : impls) {
|
for (String impl : impls) {
|
||||||
allResolved.add(impl + "." + methodName);
|
allResolved.add(impl + "." + methodName);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
allResolved.add(baseCalled);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allResolved.add(baseCalled);
|
allResolved.add(baseCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allResolved;
|
List<String> frozen = List.copyOf(allResolved);
|
||||||
|
polymorphicCallCache.put(cacheKey, frozen);
|
||||||
|
return frozen;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1958,6 +2343,44 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void qualifyBareEnumConstants(List<String> polymorphicEvents, String expectedType) {
|
||||||
|
if (expectedType == null || polymorphicEvents == null || polymorphicEvents.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> enumValues = context.getEnumValues(expectedType);
|
||||||
|
String simpleExpectedType = expectedType;
|
||||||
|
if (enumValues == null) {
|
||||||
|
simpleExpectedType = expectedType.contains(".")
|
||||||
|
? expectedType.substring(expectedType.lastIndexOf('.') + 1)
|
||||||
|
: expectedType;
|
||||||
|
enumValues = context.getEnumValues(simpleExpectedType);
|
||||||
|
} else if (expectedType.contains(".")) {
|
||||||
|
simpleExpectedType = expectedType.substring(expectedType.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
if (enumValues == null || enumValues.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < polymorphicEvents.size(); i++) {
|
||||||
|
String pe = polymorphicEvents.get(i);
|
||||||
|
if (pe == null || pe.contains(".") || pe.startsWith("<SYMBOLIC:") || pe.startsWith("ENUM_SET:")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!pe.matches("^[A-Z_][A-Z0-9_]*$")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean matchesEnum = false;
|
||||||
|
for (String enumValue : enumValues) {
|
||||||
|
if (enumValue.endsWith("." + pe)) {
|
||||||
|
matchesEnum = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchesEnum) {
|
||||||
|
polymorphicEvents.set(i, simpleExpectedType + "." + pe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveValueOfEnumTypeName(MethodInvocation mi) {
|
private String resolveValueOfEnumTypeName(MethodInvocation mi) {
|
||||||
if (mi.getExpression() instanceof SimpleName sn) {
|
if (mi.getExpression() instanceof SimpleName sn) {
|
||||||
return sn.getIdentifier();
|
return sn.getIdentifier();
|
||||||
@@ -1981,7 +2404,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
Map<String, String> parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i);
|
Map<String, String> parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i);
|
||||||
String traced = variableTracer.traceLocalVariable(target, argName, parameterValues);
|
String traced = variableTracer.traceLocalVariable(target, argName, parameterValues);
|
||||||
if (traced != null && !traced.equals(argName)) {
|
if (traced != null && !traced.equals(argName)) {
|
||||||
String normalized = normalizeEnumConstantName(traced);
|
String normalized = resolveBoundArgumentToEnumConstant(traced, caller);
|
||||||
if (normalized != null) {
|
if (normalized != null) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
@@ -1994,7 +2417,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
for (CallEdge edge : edges) {
|
for (CallEdge edge : edges) {
|
||||||
if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
|
if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||||
if (paramIndex < edge.getArguments().size()) {
|
if (paramIndex < edge.getArguments().size()) {
|
||||||
String normalized = normalizeEnumConstantName(edge.getArguments().get(paramIndex));
|
String normalized = resolveBoundArgumentToEnumConstant(
|
||||||
|
edge.getArguments().get(paramIndex), caller);
|
||||||
if (normalized != null) {
|
if (normalized != null) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
@@ -2007,6 +2431,62 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveBoundArgumentToEnumConstant(String boundValue, String scopeMethodFqn) {
|
||||||
|
if (boundValue == null || boundValue.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String fromLiteral = normalizeEnumConstantName(boundValue);
|
||||||
|
if (fromLiteral != null) {
|
||||||
|
return fromLiteral;
|
||||||
|
}
|
||||||
|
return resolveMethodCallExpressionToEnumConstant(boundValue, scopeMethodFqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveMethodCallExpressionToEnumConstant(String expr, String scopeMethodFqn) {
|
||||||
|
if (expr == null || !expr.contains("(")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ASTNode node = parseExpressionString(expr);
|
||||||
|
while (node instanceof ParenthesizedExpression pe) {
|
||||||
|
node = pe.getExpression();
|
||||||
|
}
|
||||||
|
if (!(node instanceof MethodInvocation mi) || !mi.arguments().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
String receiverType = null;
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
receiverType = variableTracer.getVariableDeclaredType(scopeMethodFqn, sn.getIdentifier());
|
||||||
|
} else if (receiver instanceof FieldAccess fa) {
|
||||||
|
receiverType = variableTracer.getVariableDeclaredType(scopeMethodFqn, fa.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
if (receiverType == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String simpleReceiverType = receiverType.contains("<")
|
||||||
|
? receiverType.substring(0, receiverType.indexOf('<'))
|
||||||
|
: receiverType;
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType);
|
||||||
|
if (td == null) {
|
||||||
|
td = context.getTypeDeclaration(receiverType);
|
||||||
|
}
|
||||||
|
if (td == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String ownerFqn = context.getFqn(td);
|
||||||
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit contextCu ? contextCu : null;
|
||||||
|
List<String> constants = constantExtractor.resolveMethodReturnConstant(
|
||||||
|
ownerFqn, methodName, 0, new HashSet<>(), cu);
|
||||||
|
if (constants.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (constants.size() == 1) {
|
||||||
|
return normalizeEnumConstantName(constants.get(0));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeEnumConstantName(String value) {
|
private String normalizeEnumConstantName(String value) {
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -2141,6 +2621,13 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void extractEventConstantsFromConstructor(ClassInstanceCreation cic, List<String> polymorphicEvents) {
|
||||||
|
if (cic.getAnonymousClassDeclaration() != null || cic.arguments().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
constantExtractor.extractConstantsFromExpression((Expression) cic.arguments().get(0), polymorphicEvents);
|
||||||
|
}
|
||||||
|
|
||||||
private void addConstantsFromTracedExpression(
|
private void addConstantsFromTracedExpression(
|
||||||
Expression expr, List<String> constants, List<String> path, Map<String, List<CallEdge>> callGraph, String scopeMethod) {
|
Expression expr, List<String> constants, List<String> path, Map<String, List<CallEdge>> callGraph, String scopeMethod) {
|
||||||
List<Expression> traced = variableTracer.traceVariableAll(expr);
|
List<Expression> traced = variableTracer.traceVariableAll(expr);
|
||||||
|
|||||||
@@ -147,19 +147,18 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> polymorphicEvents = new ArrayList<>();
|
List<String> polymorphicEvents = new ArrayList<>();
|
||||||
if (resolvedValue.matches(".*\\.[a-zA-Z0-9_]+\\(\\)")) {
|
if (!methodSuffix.isEmpty() && methodSuffix.matches("\\.[a-zA-Z0-9_]+\\(\\)")
|
||||||
String varName = resolvedValue.substring(0, resolvedValue.indexOf('.'));
|
&& currentParamName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
|
||||||
String methodName = resolvedValue.substring(resolvedValue.indexOf('.') + 1, resolvedValue.indexOf('('));
|
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
|
// Resolve in the first method in the path where the variable might be declared
|
||||||
for (String methodFqn : path) {
|
for (String methodFqn : path) {
|
||||||
String declaredType = getVariableDeclaredType(methodFqn, varName);
|
String declaredType = getVariableDeclaredType(methodFqn, varName);
|
||||||
if (declaredType != null) {
|
if (declaredType != null) {
|
||||||
System.out.println("DEEP TRACE: " + methodFqn + " " + varName + " -> " + declaredType);
|
|
||||||
List<String> typesToInspect = new ArrayList<>();
|
List<String> typesToInspect = new ArrayList<>();
|
||||||
typesToInspect.add(declaredType);
|
typesToInspect.add(declaredType);
|
||||||
typesToInspect.addAll(context.getImplementations(declaredType));
|
typesToInspect.addAll(context.getImplementations(declaredType));
|
||||||
System.out.println("DEEP TRACE IMPLS: " + typesToInspect);
|
|
||||||
|
|
||||||
for (String type : typesToInspect) {
|
for (String type : typesToInspect) {
|
||||||
Set<String> visited = new HashSet<>();
|
Set<String> visited = new HashSet<>();
|
||||||
@@ -177,7 +176,6 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
|
List<String> constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse);
|
||||||
System.out.println("DEEP TRACE RETURN: " + type + " -> " + constants);
|
|
||||||
for (String constant : constants) {
|
for (String constant : constants) {
|
||||||
if (!polymorphicEvents.contains(constant)) {
|
if (!polymorphicEvents.contains(constant)) {
|
||||||
polymorphicEvents.add(constant);
|
polymorphicEvents.add(constant);
|
||||||
@@ -265,7 +263,6 @@ public class CallGraphBuilder {
|
|||||||
List<String> constants = new ArrayList<>();
|
List<String> constants = new ArrayList<>();
|
||||||
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
|
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
|
||||||
if (td == null) {
|
if (td == null) {
|
||||||
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
|
|
||||||
}
|
}
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
@@ -279,7 +276,6 @@ public class CallGraphBuilder {
|
|||||||
if (retExpr instanceof MethodInvocation mi) {
|
if (retExpr instanceof MethodInvocation mi) {
|
||||||
// Follow delegation first
|
// Follow delegation first
|
||||||
String called = resolveCalledMethod(mi);
|
String called = resolveCalledMethod(mi);
|
||||||
System.out.println("DEEP TRACE RESOLVED CALLED: " + called);
|
|
||||||
if (called != null && called.contains(".")) {
|
if (called != null && called.contains(".")) {
|
||||||
if (visited.contains(called)) {
|
if (visited.contains(called)) {
|
||||||
handled = true;
|
handled = true;
|
||||||
@@ -549,10 +545,17 @@ public class CallGraphBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String val = constantResolver.resolve(expr, context);
|
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) {
|
if (val == null) {
|
||||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
args.add(val);
|
args.add(val);
|
||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ public class CallGraphPathFinder {
|
|||||||
this.context = context;
|
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) {
|
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 (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||||
if (!visited.add(start)) {
|
if (!visited.add(start)) {
|
||||||
@@ -360,17 +366,8 @@ public class CallGraphPathFinder {
|
|||||||
|
|
||||||
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
|
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(classTarget);
|
if (context.areClassesPolymorphicallyCompatible(classNeighbor, classTarget)) {
|
||||||
if (impls != null) {
|
return true;
|
||||||
for (String impl : impls) {
|
|
||||||
if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<String> implsN = context.getImplementations(classNeighbor);
|
|
||||||
if (implsN != null) {
|
|
||||||
for (String impl : implsN) {
|
|
||||||
if (impl.equals(classTarget) || impl.endsWith("." + classTarget)) return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -294,6 +294,17 @@ public class ConstantExtractor {
|
|||||||
Set<String> visited,
|
Set<String> visited,
|
||||||
CompilationUnit contextCu,
|
CompilationUnit contextCu,
|
||||||
ResolutionBudget budget) {
|
ResolutionBudget budget) {
|
||||||
|
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) {
|
||||||
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
|
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
|
||||||
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
||||||
if (budget == null) {
|
if (budget == null) {
|
||||||
@@ -316,9 +327,19 @@ public class ConstantExtractor {
|
|||||||
tempTd = context.getTypeDeclaration(className);
|
tempTd = context.getTypeDeclaration(className);
|
||||||
}
|
}
|
||||||
final TypeDeclaration td = tempTd;
|
final TypeDeclaration td = tempTd;
|
||||||
boolean widenToImplementations = td == null
|
boolean widenToImplementations = allowWidenToImplementations
|
||||||
|
&& (td == null
|
||||||
|| td.isInterface()
|
|| td.isInterface()
|
||||||
|| Modifier.isAbstract(td.getModifiers());
|
|| Modifier.isAbstract(td.getModifiers()));
|
||||||
|
|
||||||
|
if (widenToImplementations && depth == 0) {
|
||||||
|
Optional<List<String>> precomputed = context.getPolymorphicAccessorIndex()
|
||||||
|
.widenedReturnConstants(className, methodName);
|
||||||
|
if (precomputed.isPresent()) {
|
||||||
|
visited.remove(fqn);
|
||||||
|
return new ArrayList<>(precomputed.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<String> constants = new ArrayList<>();
|
List<String> constants = new ArrayList<>();
|
||||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
||||||
@@ -371,7 +392,7 @@ public class ConstantExtractor {
|
|||||||
|
|
||||||
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
List<String> delegationResult = resolveMethodReturnConstant(
|
List<String> delegationResult = resolveMethodReturnConstant(
|
||||||
cName, mName, depth + 1, visited, contextCu, activeBudget);
|
cName, mName, depth + 1, visited, contextCu, activeBudget, allowWidenToImplementations);
|
||||||
constants.addAll(delegationResult);
|
constants.addAll(delegationResult);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@@ -391,7 +412,7 @@ public class ConstantExtractor {
|
|||||||
|
|
||||||
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) {
|
||||||
List<String> delegationResult = resolveMethodReturnConstant(
|
List<String> delegationResult = resolveMethodReturnConstant(
|
||||||
cName, mName, depth + 1, visited, contextCu, activeBudget);
|
cName, mName, depth + 1, visited, contextCu, activeBudget, allowWidenToImplementations);
|
||||||
constants.addAll(delegationResult);
|
constants.addAll(delegationResult);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@@ -438,7 +459,7 @@ public class ConstantExtractor {
|
|||||||
if (impls != null) {
|
if (impls != null) {
|
||||||
for (String implName : impls) {
|
for (String implName : impls) {
|
||||||
List<String> delegationResult = resolveMethodReturnConstant(
|
List<String> delegationResult = resolveMethodReturnConstant(
|
||||||
implName, methodName, depth + 1, visited, contextCu, activeBudget);
|
implName, methodName, depth + 1, visited, contextCu, activeBudget, allowWidenToImplementations);
|
||||||
for (String value : delegationResult) {
|
for (String value : delegationResult) {
|
||||||
if (!constants.contains(value)) {
|
if (!constants.contains(value)) {
|
||||||
constants.add(value);
|
constants.add(value);
|
||||||
@@ -472,6 +493,13 @@ public class ConstantExtractor {
|
|||||||
String requestingTypeFqn,
|
String requestingTypeFqn,
|
||||||
CompilationUnit contextCu,
|
CompilationUnit contextCu,
|
||||||
Set<String> visited) {
|
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(
|
return AccessorFieldResolver.resolveFieldConstants(
|
||||||
accessor,
|
accessor,
|
||||||
requestingTypeFqn,
|
requestingTypeFqn,
|
||||||
@@ -508,9 +536,14 @@ public class ConstantExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<String> implementations = context.getImplementations(className);
|
List<String> implementations = context.getImplementations(className);
|
||||||
if (implementations == null) {
|
if (implementations == null || implementations.isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
Optional<AccessorSummary> precomputed = context.getPolymorphicAccessorIndex()
|
||||||
|
.representativeImplAccessor(className, methodName);
|
||||||
|
if (precomputed.isPresent()) {
|
||||||
|
return precomputed;
|
||||||
|
}
|
||||||
for (String implementation : implementations) {
|
for (String implementation : implementations) {
|
||||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
||||||
if (implAccessor.isPresent()) {
|
if (implAccessor.isPresent()) {
|
||||||
@@ -551,6 +584,20 @@ public class ConstantExtractor {
|
|||||||
return constants;
|
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) {
|
public String parseEnumSetElement(String eVal) {
|
||||||
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : 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 final ConstantResolver constantResolver;
|
||||||
private VariableTracer variableTracer;
|
private VariableTracer variableTracer;
|
||||||
private ConstantExtractor constantExtractor;
|
private ConstantExtractor constantExtractor;
|
||||||
|
private final Map<String, ClassInstanceCreation> returnedCicCache = new HashMap<>();
|
||||||
|
private final Set<String> returnedCicAbsent = new HashSet<>();
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface RhsResolver {
|
public interface RhsResolver {
|
||||||
@@ -33,6 +35,11 @@ public class ConstructorAnalyzer {
|
|||||||
this.constantExtractor = constantExtractor;
|
this.constantExtractor = constantExtractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clearReturnedCicCache() {
|
||||||
|
returnedCicCache.clear();
|
||||||
|
returnedCicAbsent.clear();
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
return traceFieldInConstructors(td, fieldName, context, visited, ResolutionBudget.defaults());
|
return traceFieldInConstructors(td, fieldName, context, visited, ResolutionBudget.defaults());
|
||||||
}
|
}
|
||||||
@@ -480,7 +487,20 @@ public class ConstructorAnalyzer {
|
|||||||
|
|
||||||
public ClassInstanceCreation findReturnedCIC(
|
public ClassInstanceCreation findReturnedCIC(
|
||||||
String className, String methodName, CodebaseContext context) {
|
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(
|
private ClassInstanceCreation findReturnedCIC(
|
||||||
|
|||||||
@@ -141,32 +141,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return graph;
|
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) {
|
private String findDefiningClass(String className, String methodName) {
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
if (td == null) return null;
|
if (td == null) return null;
|
||||||
@@ -230,351 +204,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return null;
|
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) {
|
protected String resolveMethodInvocationReturnType(MethodInvocation mi) {
|
||||||
Expression receiver = mi.getExpression();
|
Expression receiver = mi.getExpression();
|
||||||
String receiverType = null;
|
String receiverType = null;
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
List<String> args = resolveArguments(node.arguments());
|
List<String> args = resolveArguments(node.arguments());
|
||||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
||||||
for (String calledMethod : calledMethods) {
|
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));
|
edge.setConstraint(resolveConstraint(node, calledMethod, constraint));
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||||
}
|
}
|
||||||
@@ -64,7 +65,8 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
} else {
|
} else {
|
||||||
implicitArgs.addAll(args);
|
implicitArgs.addAll(args);
|
||||||
}
|
}
|
||||||
CallEdge edge = new CallEdge(refMethod, implicitArgs);
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
CallEdge edge = new CallEdge(refMethod, implicitArgs, receiver);
|
||||||
edge.setConstraint(constraint);
|
edge.setConstraint(constraint);
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||||
}
|
}
|
||||||
@@ -85,13 +87,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
} else {
|
} else {
|
||||||
implicitArgs.addAll(args);
|
implicitArgs.addAll(args);
|
||||||
}
|
}
|
||||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs);
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
CallEdge edge = new CallEdge(calledMethod, implicitArgs, receiver);
|
||||||
edge.setConstraint(constraint);
|
edge.setConstraint(constraint);
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||||
for (String impl : impls) {
|
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);
|
implEdge.setConstraint(constraint);
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(implEdge);
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(implEdge);
|
||||||
}
|
}
|
||||||
@@ -125,8 +128,9 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
calledMethod = superFqn + "." + methodName;
|
calledMethod = superFqn + "." + methodName;
|
||||||
}
|
}
|
||||||
List<String> args = resolveArguments(node.arguments());
|
List<String> args = resolveArguments(node.arguments());
|
||||||
|
String receiver = "super";
|
||||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils.findConditionConstraint(node);
|
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);
|
edge.setConstraint(constraint);
|
||||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(edge);
|
||||||
}
|
}
|
||||||
@@ -260,88 +264,6 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
return null;
|
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) {
|
private Expression unwrapMessageBuilder(Expression expr) {
|
||||||
if (expr instanceof MethodInvocation mi) {
|
if (expr instanceof MethodInvocation mi) {
|
||||||
MethodInvocation current = mi;
|
MethodInvocation current = mi;
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
private final SpringComponentDetector componentDetector;
|
private final SpringComponentDetector componentDetector;
|
||||||
private List<TriggerPoint> cachedTriggers;
|
private List<TriggerPoint> cachedTriggers;
|
||||||
private List<EntryPoint> cachedEntryPoints;
|
private List<EntryPoint> cachedEntryPoints;
|
||||||
private List<CallChain> cachedCallChains;
|
|
||||||
|
|
||||||
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@@ -94,17 +93,15 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
if (cachedCallChains != null) {
|
List<EntryPoint> scopedEntryPoints = entryPoints != null && !entryPoints.isEmpty()
|
||||||
return cachedCallChains;
|
? entryPoints
|
||||||
}
|
: findEntryPoints();
|
||||||
List<EntryPoint> allEntryPoints = findEntryPoints();
|
List<TriggerPoint> scopedTriggers = triggers != null && !triggers.isEmpty()
|
||||||
List<TriggerPoint> allTriggers = triggers != null && !triggers.isEmpty()
|
|
||||||
? triggers
|
? triggers
|
||||||
: findTriggerPoints();
|
: findTriggerPoints();
|
||||||
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers",
|
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers",
|
||||||
allEntryPoints.size(), allTriggers.size());
|
scopedEntryPoints.size(), scopedTriggers.size());
|
||||||
cachedCallChains = callGraphEngine.findChains(allEntryPoints, allTriggers);
|
return callGraphEngine.findChains(scopedEntryPoints, scopedTriggers);
|
||||||
return cachedCallChains;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class SpringMvcDetector {
|
|||||||
cu.accept(new ASTVisitor() {
|
cu.accept(new ASTVisitor() {
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(TypeDeclaration node) {
|
public boolean visit(TypeDeclaration node) {
|
||||||
if (isRestController(node)) {
|
if (isRestController(node) && !node.isInterface()) {
|
||||||
processController(node, entryPoints);
|
processController(node, entryPoints);
|
||||||
}
|
}
|
||||||
// Support WebFlux RouterFunction beans
|
// Support WebFlux RouterFunction beans
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver
|
|||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
|
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ public class VariableTracer {
|
|||||||
private ConstantExtractor constantExtractor;
|
private ConstantExtractor constantExtractor;
|
||||||
|
|
||||||
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
|
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
|
||||||
|
private final Map<String, String> variableDeclaredTypeCache = new HashMap<>();
|
||||||
|
|
||||||
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
|
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@@ -27,6 +29,13 @@ public class VariableTracer {
|
|||||||
this.constantExtractor = constantExtractor;
|
this.constantExtractor = constantExtractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clearAnalysisCaches() {
|
||||||
|
variableDeclaredTypeCache.clear();
|
||||||
|
if (dataFlowModel instanceof JdtDataFlowModel jdtDataFlowModel) {
|
||||||
|
jdtDataFlowModel.clearAnalysisCaches();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Expression traceVariable(Expression expr) {
|
public Expression traceVariable(Expression expr) {
|
||||||
List<Expression> all = traceVariableAll(expr);
|
List<Expression> all = traceVariableAll(expr);
|
||||||
return all.isEmpty() ? expr : all.get(all.size() - 1);
|
return all.isEmpty() ? expr : all.get(all.size() - 1);
|
||||||
@@ -159,9 +168,18 @@ public class VariableTracer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getFieldType(TypeDeclaration td, String fieldName, CodebaseContext context, java.util.Set<String> visited) {
|
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);
|
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 (FieldDeclaration fd : td.getFields()) {
|
||||||
for (Object fragObj : fd.fragments()) {
|
for (Object fragObj : fd.fragments()) {
|
||||||
@@ -215,7 +233,21 @@ public class VariableTracer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
|
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('.');
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
if (lastDot > 0) {
|
if (lastDot > 0) {
|
||||||
String fqn = methodFqn.substring(0, lastDot);
|
String fqn = methodFqn.substring(0, lastDot);
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ package click.kamil.springstatemachineexporter.ast.common;
|
|||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndex;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndex;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndexBuilder;
|
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.model.LibraryHint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
|
||||||
@@ -29,6 +40,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class CodebaseContext {
|
public class CodebaseContext {
|
||||||
private final Map<String, CompilationUnit> classes = new HashMap<>();
|
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, org.eclipse.jdt.core.dom.RecordDeclaration> recordDeclarations = new HashMap<>();
|
||||||
private final Map<String, Path> classPaths = new HashMap<>();
|
private final Map<String, Path> classPaths = new HashMap<>();
|
||||||
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
private final Map<String, String> simpleNameToFqn = new HashMap<>();
|
||||||
@@ -47,6 +59,16 @@ public class CodebaseContext {
|
|||||||
private Path projectRoot;
|
private Path projectRoot;
|
||||||
private final Map<String, Object> cache = new HashMap<>();
|
private final Map<String, Object> cache = new HashMap<>();
|
||||||
private AccessorIndex accessorIndex = AccessorIndex.empty();
|
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;
|
private boolean inlineAccessors = true;
|
||||||
|
|
||||||
public Map<String, Object> getCache() {
|
public Map<String, Object> getCache() {
|
||||||
@@ -57,6 +79,40 @@ public class CodebaseContext {
|
|||||||
return accessorIndex;
|
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() {
|
public boolean isInlineAccessors() {
|
||||||
return inlineAccessors;
|
return inlineAccessors;
|
||||||
}
|
}
|
||||||
@@ -241,6 +297,7 @@ public class CodebaseContext {
|
|||||||
|
|
||||||
List<Path> javaFiles = FileUtils.findFilesWithExtension(rootDirs, ".java", activeIgnore);
|
List<Path> javaFiles = FileUtils.findFilesWithExtension(rootDirs, ".java", activeIgnore);
|
||||||
log.info("CodebaseContext found {} java files across {} root directories", javaFiles.size(), rootDirs.size());
|
log.info("CodebaseContext found {} java files across {} root directories", javaFiles.size(), rootDirs.size());
|
||||||
|
typeDeclarations.clear();
|
||||||
for (Path javaFile : javaFiles) {
|
for (Path javaFile : javaFiles) {
|
||||||
String source = Files.readString(javaFile);
|
String source = Files.readString(javaFile);
|
||||||
CompilationUnit cu = parse(source, javaFile.toAbsolutePath().toString());
|
CompilationUnit cu = parse(source, javaFile.toAbsolutePath().toString());
|
||||||
@@ -258,9 +315,22 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.implementationsCache.clear();
|
||||||
this.accessorIndex = inlineAccessors
|
this.accessorIndex = inlineAccessors
|
||||||
? new AccessorIndexBuilder().build(this)
|
? new AccessorIndexBuilder().build(this)
|
||||||
: AccessorIndex.empty();
|
: 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) {
|
if (inlineAccessors) {
|
||||||
log.info("Built accessor index: {} trivial accessors across {} types",
|
log.info("Built accessor index: {} trivial accessors across {} types",
|
||||||
accessorIndex.trivialCount(), accessorIndex.typeCount());
|
accessorIndex.trivialCount(), accessorIndex.typeCount());
|
||||||
@@ -275,6 +345,7 @@ public class CodebaseContext {
|
|||||||
|
|
||||||
classes.put(fqn, (CompilationUnit) td.getRoot());
|
classes.put(fqn, (CompilationUnit) td.getRoot());
|
||||||
classPaths.put(fqn, javaFile);
|
classPaths.put(fqn, javaFile);
|
||||||
|
typeDeclarations.put(fqn, td);
|
||||||
|
|
||||||
if (simpleNameToFqn.containsKey(simpleName)) {
|
if (simpleNameToFqn.containsKey(simpleName)) {
|
||||||
String existingFqn = simpleNameToFqn.get(simpleName);
|
String existingFqn = simpleNameToFqn.get(simpleName);
|
||||||
@@ -343,15 +414,21 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getImplementations(String interfaceName) {
|
public List<String> getImplementations(String interfaceName) {
|
||||||
String cleanName = interfaceName;
|
if (interfaceName == null) {
|
||||||
if (interfaceName != null && interfaceName.contains("<")) {
|
return List.of();
|
||||||
cleanName = interfaceName.substring(0, interfaceName.indexOf('<'));
|
|
||||||
}
|
}
|
||||||
|
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<>();
|
Set<String> allImpls = new LinkedHashSet<>();
|
||||||
collectImplementations(cleanName, allImpls, new HashSet<>());
|
collectImplementations(cleanName, allImpls, new HashSet<>());
|
||||||
List<String> sorted = new ArrayList<>(allImpls);
|
List<String> sorted = new ArrayList<>(allImpls);
|
||||||
Collections.sort(sorted);
|
Collections.sort(sorted);
|
||||||
return sorted;
|
return Collections.unmodifiableList(sorted);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
|
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
|
||||||
@@ -465,10 +542,20 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public TypeDeclaration getTypeDeclaration(String name) {
|
public TypeDeclaration getTypeDeclaration(String name) {
|
||||||
|
TypeDeclaration cached = typeDeclarations.get(name);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
// Try exact FQN match first
|
// Try exact FQN match first
|
||||||
CompilationUnit cu = classes.get(name);
|
CompilationUnit cu = classes.get(name);
|
||||||
if (cu != null)
|
if (cu != null) {
|
||||||
return findTypeInCu(cu, name);
|
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 it's a simple name, check if it's ambiguous
|
||||||
if (ambiguousSimpleNames.contains(name)) {
|
if (ambiguousSimpleNames.contains(name)) {
|
||||||
@@ -479,9 +566,18 @@ public class CodebaseContext {
|
|||||||
// If not ambiguous, return the one we found during scan
|
// If not ambiguous, return the one we found during scan
|
||||||
String fqn = simpleNameToFqn.get(name);
|
String fqn = simpleNameToFqn.get(name);
|
||||||
if (fqn != null) {
|
if (fqn != null) {
|
||||||
|
cached = typeDeclarations.get(fqn);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
cu = classes.get(fqn);
|
cu = classes.get(fqn);
|
||||||
if (cu != null)
|
if (cu != null) {
|
||||||
return findTypeInCu(cu, fqn);
|
TypeDeclaration resolved = findTypeInCu(cu, fqn);
|
||||||
|
if (resolved != null) {
|
||||||
|
typeDeclarations.put(fqn, resolved);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -541,7 +637,21 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getSuperclassFqn(TypeDeclaration td) {
|
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();
|
ITypeBinding binding = td.resolveBinding();
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
ITypeBinding superBinding = binding.getSuperclass();
|
ITypeBinding superBinding = binding.getSuperclass();
|
||||||
@@ -571,7 +681,23 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
|
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();
|
ITypeBinding binding = td.resolveBinding();
|
||||||
if (binding != null && includeSuper) {
|
if (binding != null && includeSuper) {
|
||||||
IMethodBinding methodBinding = findMethodBinding(binding, methodName);
|
IMethodBinding methodBinding = findMethodBinding(binding, methodName);
|
||||||
@@ -650,6 +776,44 @@ public class CodebaseContext {
|
|||||||
return null;
|
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) {
|
public org.eclipse.jdt.core.dom.RecordDeclaration getRecordDeclaration(String fqn) {
|
||||||
if (fqn == null || fqn.isEmpty()) {
|
if (fqn == null || fqn.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
|
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
|
||||||
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
||||||
private final Map<ASTNode, Map<IVariableBinding, Expression>> objectFieldBindings = 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<>();
|
private static final ThreadLocal<List<String>> CURRENT_PATH = new ThreadLocal<>();
|
||||||
|
|
||||||
@@ -33,6 +34,10 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clearAnalysisCaches() {
|
||||||
|
compatibleContextTypesCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
ASTNode current = node;
|
ASTNode current = node;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
@@ -1514,6 +1519,12 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> getCompatibleContextTypes(List<String> currentPath, String declaringClassFqn) {
|
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<>();
|
Set<String> contextTypes = new HashSet<>();
|
||||||
List<String> implementations = context.getImplementations(declaringClassFqn);
|
List<String> implementations = context.getImplementations(declaringClassFqn);
|
||||||
|
|
||||||
|
|||||||
@@ -45,4 +45,44 @@ class MachineScopeFilterEntryPointTest {
|
|||||||
.contains("POST /api/orders/pay", "POST /api/commands/{commandKey}")
|
.contains("POST /api/orders/pay", "POST /api/commands/{commandKey}")
|
||||||
.doesNotContain("POST /api/documents/submit");
|
.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;
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.junit.jupiter.api.Tag;
|
import org.junit.jupiter.api.Tag;
|
||||||
import org.junit.jupiter.api.Test;
|
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.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@@ -165,4 +167,37 @@ public class HeuristicBeanResolutionEngineTest {
|
|||||||
engine.isRoutedToCorrectMachine(chain, "com.example.DocumentStateMachineConfig", context),
|
engine.isRoutedToCorrectMachine(chain, "com.example.DocumentStateMachineConfig", context),
|
||||||
"Ambiguous shared gateway should not attach to every machine");
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,7 +97,7 @@ class AnalysisCacheCorrectnessTest {
|
|||||||
List<CallChain> first = intelligence.findCallChains(entryPoints, triggers);
|
List<CallChain> first = intelligence.findCallChains(entryPoints, triggers);
|
||||||
List<CallChain> second = intelligence.findCallChains(entryPoints, triggers);
|
List<CallChain> second = intelligence.findCallChains(entryPoints, triggers);
|
||||||
|
|
||||||
assertThat(second).isSameAs(first);
|
assertThat(second).isEqualTo(first);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
* <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.
|
* REST → dispatcher → sendEvent pipeline without opening another module.
|
||||||
*
|
*
|
||||||
* <p>DTO setter/getter propagation is covered by {@code HeuristicCallGraphEngineTypeTest}.
|
* <p>Cross-hop routing helpers and DTO constructor→getter propagation are covered by Tier 1 tests below.
|
||||||
* Cross-hop routing helpers that return computed strings are covered by literal-forwarding tests above.
|
|
||||||
*/
|
*/
|
||||||
class CentralDispatcherResolutionTest {
|
class CentralDispatcherResolutionTest {
|
||||||
|
|
||||||
@@ -418,6 +417,105 @@ class CentralDispatcherResolutionTest {
|
|||||||
"OrderEvent.PAY");
|
"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.
|
* Chained field-backed trivial getters ({@code return this.field}) through a central dispatcher.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ final class CentralDispatcherTestSupport {
|
|||||||
private CentralDispatcherTestSupport() {
|
private CentralDispatcherTestSupport() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EngineKind {
|
||||||
|
HEURISTIC,
|
||||||
|
JDT
|
||||||
|
}
|
||||||
|
|
||||||
static CodebaseContext scanSource(String source, Path tempDir) throws Exception {
|
static CodebaseContext scanSource(String source, Path tempDir) throws Exception {
|
||||||
Files.writeString(tempDir.resolve("App.java"), source);
|
Files.writeString(tempDir.resolve("App.java"), source);
|
||||||
CodebaseContext context = new CodebaseContext();
|
CodebaseContext context = new CodebaseContext();
|
||||||
@@ -38,7 +43,18 @@ final class CentralDispatcherTestSupport {
|
|||||||
String controllerClass,
|
String controllerClass,
|
||||||
String controllerMethod,
|
String controllerMethod,
|
||||||
String stateMachineClass) {
|
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()
|
EntryPoint entryPoint = EntryPoint.builder()
|
||||||
.className(controllerClass)
|
.className(controllerClass)
|
||||||
.methodName(controllerMethod)
|
.methodName(controllerMethod)
|
||||||
@@ -50,7 +66,8 @@ final class CentralDispatcherTestSupport {
|
|||||||
.build();
|
.build();
|
||||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
assertThat(chains)
|
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);
|
.hasSize(1);
|
||||||
return chains.get(0);
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,4 +71,40 @@ class SpringMvcDetectorTest {
|
|||||||
assertThat(entryPoints).anyMatch(ep ->
|
assertThat(entryPoints).anyMatch(ep ->
|
||||||
"POST /api/orders/pay".equals(ep.getName()) && "pay".equals(ep.getMethodName()));
|
"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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,120 +147,6 @@
|
|||||||
} ]
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"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" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||||
|
|||||||
@@ -182,8 +182,112 @@
|
|||||||
"stateTypeFqn" : "String",
|
"stateTypeFqn" : "String",
|
||||||
"eventTypeFqn" : "String",
|
"eventTypeFqn" : "String",
|
||||||
"metadata" : {
|
"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" : [ {
|
"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",
|
"type" : "CUSTOM",
|
||||||
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
|
"name" : "INTERCEPTOR: SecurityInterceptor.preHandle",
|
||||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||||
@@ -239,7 +343,222 @@
|
|||||||
"annotations" : [ ]
|
"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" : {
|
"properties" : {
|
||||||
"default" : { }
|
"default" : { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,40 +182,6 @@
|
|||||||
} ]
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"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" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /api/payment/{id}/capture",
|
"name" : "POST /api/payment/{id}/capture",
|
||||||
|
|||||||
@@ -182,40 +182,6 @@
|
|||||||
} ]
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"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" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /api/payment/{id}/capture",
|
"name" : "POST /api/payment/{id}/capture",
|
||||||
|
|||||||
@@ -32,9 +32,67 @@
|
|||||||
"stateTypeFqn" : "String",
|
"stateTypeFqn" : "String",
|
||||||
"eventTypeFqn" : "String",
|
"eventTypeFqn" : "String",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"triggers" : [ ],
|
"triggers" : [ {
|
||||||
"entryPoints" : [ ],
|
"event" : "java.lang.String.INHERITED_SUBMIT",
|
||||||
"callChains" : [ ],
|
"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" : {
|
"properties" : {
|
||||||
"default" : { }
|
"default" : { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,61 @@
|
|||||||
"eventTypeFqn" : "String",
|
"eventTypeFqn" : "String",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"triggers" : [ ],
|
"triggers" : [ ],
|
||||||
"entryPoints" : [ ],
|
"entryPoints" : [ {
|
||||||
"callChains" : [ ],
|
"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" : {
|
"properties" : {
|
||||||
"default" : { }
|
"default" : { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
"rawName" : "\"BUSY\"",
|
"rawName" : "\"BUSY\"",
|
||||||
"fullIdentifier" : "BUSY"
|
"fullIdentifier" : "BUSY"
|
||||||
} ],
|
} ],
|
||||||
"event" : "ORDER_EVENT",
|
"event" : "SUBMIT",
|
||||||
"guard" : null,
|
"guard" : null,
|
||||||
"actions" : [ {
|
"actions" : [ {
|
||||||
"expression" : "loggingAction()",
|
"expression" : "loggingAction()",
|
||||||
@@ -61,6 +61,20 @@
|
|||||||
"lineNumber" : 23
|
"lineNumber" : 23
|
||||||
} ],
|
} ],
|
||||||
"order" : null
|
"order" : null
|
||||||
|
}, {
|
||||||
|
"type" : "EXTERNAL",
|
||||||
|
"sourceStates" : [ {
|
||||||
|
"rawName" : "\"BUSY\"",
|
||||||
|
"fullIdentifier" : "BUSY"
|
||||||
|
} ],
|
||||||
|
"targetStates" : [ {
|
||||||
|
"rawName" : "\"DONE\"",
|
||||||
|
"fullIdentifier" : "DONE"
|
||||||
|
} ],
|
||||||
|
"event" : "ORDER_EVENT",
|
||||||
|
"guard" : null,
|
||||||
|
"actions" : [ ],
|
||||||
|
"order" : null
|
||||||
} ],
|
} ],
|
||||||
"endStates" : [ "DONE" ]
|
"endStates" : [ "DONE" ]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,20 @@
|
|||||||
"stateTypeFqn" : "String",
|
"stateTypeFqn" : "String",
|
||||||
"eventTypeFqn" : "String",
|
"eventTypeFqn" : "String",
|
||||||
"metadata" : {
|
"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" : [ {
|
"entryPoints" : [ {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /orders/submit",
|
"name" : "POST /orders/submit",
|
||||||
@@ -75,12 +88,14 @@
|
|||||||
"type" : "String",
|
"type" : "String",
|
||||||
"annotations" : [ "RequestBody" ]
|
"annotations" : [ "RequestBody" ]
|
||||||
} ]
|
} ]
|
||||||
}, {
|
} ],
|
||||||
|
"callChains" : [ {
|
||||||
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /orders/submit",
|
"name" : "POST /orders/submit",
|
||||||
"className" : "click.kamil.multi.api.OrderApi",
|
"className" : "click.kamil.multi.core.OrderController",
|
||||||
"methodName" : "submitOrder",
|
"methodName" : "submitOrder",
|
||||||
"sourceFile" : "api-module/src/main/java/click/kamil/multi/api/OrderApi.java",
|
"sourceFile" : "core-module/src/main/java/click/kamil/multi/core/OrderController.java",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"path" : "/orders/submit",
|
"path" : "/orders/submit",
|
||||||
"verb" : "POST"
|
"verb" : "POST"
|
||||||
@@ -90,8 +105,29 @@
|
|||||||
"type" : "String",
|
"type" : "String",
|
||||||
"annotations" : [ "RequestBody" ]
|
"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" : {
|
"properties" : {
|
||||||
"default" : { }
|
"default" : { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,82 +124,6 @@
|
|||||||
} ]
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"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" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||||
|
|||||||
@@ -87,6 +87,39 @@
|
|||||||
"ambiguous" : false
|
"ambiguous" : false
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"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",
|
"type" : "REST",
|
||||||
"name" : "POST /api/commands/{commandKey}",
|
"name" : "POST /api/commands/{commandKey}",
|
||||||
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
|
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
|
||||||
|
|||||||
@@ -87,6 +87,83 @@
|
|||||||
"ambiguous" : false
|
"ambiguous" : false
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"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",
|
"type" : "REST",
|
||||||
"name" : "POST /api/commands/{commandKey}",
|
"name" : "POST /api/commands/{commandKey}",
|
||||||
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
|
"className" : "click.kamil.examples.statemachine.layered.web.GenericCommandController",
|
||||||
|
|||||||
@@ -130,6 +130,50 @@
|
|||||||
"ambiguous" : false
|
"ambiguous" : false
|
||||||
} ],
|
} ],
|
||||||
"entryPoints" : [ {
|
"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",
|
"type" : "RABBIT",
|
||||||
"name" : "RABBIT: order.custom.transition.queue",
|
"name" : "RABBIT: order.custom.transition.queue",
|
||||||
"className" : "click.kamil.web.RabbitOrderListener",
|
"className" : "click.kamil.web.RabbitOrderListener",
|
||||||
@@ -342,13 +386,17 @@
|
|||||||
"stateMachineId" : null,
|
"stateMachineId" : null,
|
||||||
"sourceState" : null,
|
"sourceState" : null,
|
||||||
"lineNumber" : 78,
|
"lineNumber" : 78,
|
||||||
"polymorphicEvents" : [ ],
|
"polymorphicEvents" : [ "click.kamil.domain.OrderEvent.PROCESS" ],
|
||||||
"external" : false,
|
"external" : false,
|
||||||
"constraint" : null,
|
"constraint" : null,
|
||||||
"ambiguous" : false
|
"ambiguous" : false
|
||||||
},
|
},
|
||||||
"contextMachineId" : null,
|
"contextMachineId" : null,
|
||||||
"matchedTransitions" : null
|
"matchedTransitions" : [ {
|
||||||
|
"sourceState" : "click.kamil.domain.OrderState.NEW",
|
||||||
|
"targetState" : "click.kamil.domain.OrderState.PROCESSING",
|
||||||
|
"event" : "click.kamil.domain.OrderEvent.PROCESS"
|
||||||
|
} ]
|
||||||
}, {
|
}, {
|
||||||
"entryPoint" : {
|
"entryPoint" : {
|
||||||
"type" : "JMS",
|
"type" : "JMS",
|
||||||
|
|||||||
@@ -117,78 +117,6 @@
|
|||||||
} ]
|
} ]
|
||||||
} ],
|
} ],
|
||||||
"callChains" : [ {
|
"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" : {
|
"entryPoint" : {
|
||||||
"type" : "REST",
|
"type" : "REST",
|
||||||
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
"name" : "POST /api/machine/{machineType}/transition/{event}",
|
||||||
|
|||||||
Reference in New Issue
Block a user