C
This commit is contained in:
@@ -69,6 +69,7 @@ public class CallChainEnricher implements AnalysisEnricher {
|
||||
}
|
||||
|
||||
List<CallChain> chains = intelligence.findCallChains(entryPoints, scopedTriggers);
|
||||
// Semantic deduplication runs in TransitionLinkerEnricher after transitions are linked.
|
||||
return MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Collapses call chains that represent the same logical entrypoint-to-transition link
|
||||
* but were discovered via different call-graph paths.
|
||||
*/
|
||||
public final class SemanticCallChainDedup {
|
||||
|
||||
public enum KeepStrategy {
|
||||
SHORTEST,
|
||||
FIRST
|
||||
}
|
||||
|
||||
private SemanticCallChainDedup() {
|
||||
}
|
||||
|
||||
public static List<CallChain> deduplicate(List<CallChain> chains) {
|
||||
return deduplicate(chains, KeepStrategy.SHORTEST);
|
||||
}
|
||||
|
||||
public static List<CallChain> deduplicate(List<CallChain> chains, KeepStrategy strategy) {
|
||||
if (chains == null || chains.isEmpty()) {
|
||||
return chains == null ? List.of() : chains;
|
||||
}
|
||||
Map<String, CallChain> byKey = new LinkedHashMap<>();
|
||||
for (CallChain chain : chains) {
|
||||
if (chain == null) {
|
||||
continue;
|
||||
}
|
||||
String key = semanticKey(chain);
|
||||
CallChain existing = byKey.get(key);
|
||||
if (existing == null) {
|
||||
byKey.put(key, chain);
|
||||
} else {
|
||||
byKey.put(key, pickPreferred(existing, chain, strategy));
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(byKey.values());
|
||||
}
|
||||
|
||||
public static List<CallChain> merge(
|
||||
List<CallChain> existing,
|
||||
List<CallChain> refreshed,
|
||||
KeepStrategy strategy) {
|
||||
List<CallChain> combined = new ArrayList<>();
|
||||
if (existing != null) {
|
||||
combined.addAll(existing);
|
||||
}
|
||||
if (refreshed != null) {
|
||||
combined.addAll(refreshed);
|
||||
}
|
||||
return deduplicate(combined, strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON refresh path: when refresh produces any chains for an entry point, those replace all
|
||||
* existing chains for that entry (so unresolved stubs like {@code event} do not linger).
|
||||
* Multiple refreshed chains per entry (multi-event fan-out) are preserved, then semantic
|
||||
* dedup collapses path variants.
|
||||
*/
|
||||
public static List<CallChain> mergeRefresh(
|
||||
List<CallChain> existing,
|
||||
List<CallChain> refreshed,
|
||||
KeepStrategy strategy) {
|
||||
Map<String, List<CallChain>> existingByEntry = groupByEntryPoint(existing);
|
||||
Map<String, List<CallChain>> refreshedByEntry = groupByEntryPoint(refreshed);
|
||||
LinkedHashSet<String> entries = new LinkedHashSet<>();
|
||||
entries.addAll(existingByEntry.keySet());
|
||||
entries.addAll(refreshedByEntry.keySet());
|
||||
|
||||
List<CallChain> combined = new ArrayList<>();
|
||||
for (String entryKey : entries) {
|
||||
if (refreshedByEntry.containsKey(entryKey)) {
|
||||
combined.addAll(refreshedByEntry.get(entryKey));
|
||||
} else {
|
||||
combined.addAll(existingByEntry.getOrDefault(entryKey, List.of()));
|
||||
}
|
||||
}
|
||||
return deduplicate(combined, strategy);
|
||||
}
|
||||
|
||||
private static Map<String, List<CallChain>> groupByEntryPoint(List<CallChain> chains) {
|
||||
Map<String, List<CallChain>> byEntry = new LinkedHashMap<>();
|
||||
if (chains == null) {
|
||||
return byEntry;
|
||||
}
|
||||
for (CallChain chain : chains) {
|
||||
if (chain == null) {
|
||||
continue;
|
||||
}
|
||||
byEntry.computeIfAbsent(entryPointKey(chain), key -> new ArrayList<>()).add(chain);
|
||||
}
|
||||
return byEntry;
|
||||
}
|
||||
|
||||
static String semanticKey(CallChain chain) {
|
||||
StringBuilder key = new StringBuilder();
|
||||
key.append(entryPointKey(chain)).append('|');
|
||||
key.append(triggerKey(chain.getTriggerPoint())).append('|');
|
||||
key.append(linkResolutionKey(chain.getLinkResolution())).append('|');
|
||||
key.append(matchedTransitionsKey(chain.getMatchedTransitions())).append('|');
|
||||
key.append(nullToEmpty(chain.getContextMachineId()));
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
private static CallChain pickPreferred(CallChain existing, CallChain candidate, KeepStrategy strategy) {
|
||||
if (strategy == KeepStrategy.FIRST) {
|
||||
return existing;
|
||||
}
|
||||
boolean existingAnchored = startsAtEntryPoint(existing);
|
||||
boolean candidateAnchored = startsAtEntryPoint(candidate);
|
||||
if (candidateAnchored != existingAnchored) {
|
||||
return candidateAnchored ? candidate : existing;
|
||||
}
|
||||
int existingLen = chainLength(existing);
|
||||
int candidateLen = chainLength(candidate);
|
||||
if (candidateLen < existingLen) {
|
||||
return candidate;
|
||||
}
|
||||
if (candidateLen > existingLen) {
|
||||
return existing;
|
||||
}
|
||||
int existingRichness = metadataRichness(existing);
|
||||
int candidateRichness = metadataRichness(candidate);
|
||||
if (candidateRichness != existingRichness) {
|
||||
return candidateRichness > existingRichness ? candidate : existing;
|
||||
}
|
||||
List<String> existingChain = existing.getMethodChain();
|
||||
List<String> candidateChain = candidate.getMethodChain();
|
||||
if (existingChain == null) {
|
||||
return candidate;
|
||||
}
|
||||
if (candidateChain == null) {
|
||||
return existing;
|
||||
}
|
||||
return candidateChain.toString().compareTo(existingChain.toString()) < 0 ? candidate : existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the first hop equals the entry point method identity
|
||||
* ({@code className.methodName} or {@code className#methodName} key form).
|
||||
*/
|
||||
static boolean startsAtEntryPoint(CallChain chain) {
|
||||
if (chain == null) {
|
||||
return false;
|
||||
}
|
||||
EntryPoint entryPoint = chain.getEntryPoint();
|
||||
List<String> methodChain = chain.getMethodChain();
|
||||
if (entryPoint == null
|
||||
|| entryPoint.getClassName() == null
|
||||
|| entryPoint.getMethodName() == null
|
||||
|| methodChain == null
|
||||
|| methodChain.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String firstHop = methodChain.get(0);
|
||||
if (firstHop == null || firstHop.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String entryHop = entryPoint.getClassName() + "." + entryPoint.getMethodName();
|
||||
if (entryHop.equals(firstHop)) {
|
||||
return true;
|
||||
}
|
||||
return normalizeMethodHopToEntryKey(firstHop)
|
||||
.equals(entryPoint.getClassName() + "#" + entryPoint.getMethodName());
|
||||
}
|
||||
|
||||
private static int metadataRichness(CallChain chain) {
|
||||
int score = 0;
|
||||
if (chain.getEntryPoint() != null) {
|
||||
score += 2;
|
||||
}
|
||||
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty()) {
|
||||
score += 1;
|
||||
}
|
||||
if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
|
||||
score += 4;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
private static int chainLength(CallChain chain) {
|
||||
return chain.getMethodChain() == null ? Integer.MAX_VALUE : chain.getMethodChain().size();
|
||||
}
|
||||
|
||||
private static String entryPointKey(CallChain chain) {
|
||||
EntryPoint entryPoint = chain.getEntryPoint();
|
||||
if (entryPoint != null) {
|
||||
if (entryPoint.getName() != null && !entryPoint.getName().isBlank()) {
|
||||
return entryPoint.getName();
|
||||
}
|
||||
if (entryPoint.getClassName() != null && entryPoint.getMethodName() != null) {
|
||||
return entryPoint.getClassName() + "#" + entryPoint.getMethodName();
|
||||
}
|
||||
}
|
||||
List<String> methodChain = chain.getMethodChain();
|
||||
if (methodChain != null && !methodChain.isEmpty()) {
|
||||
return normalizeMethodHopToEntryKey(methodChain.get(0));
|
||||
}
|
||||
TriggerPoint trigger = chain.getTriggerPoint();
|
||||
if (trigger != null && trigger.getClassName() != null && trigger.getMethodName() != null) {
|
||||
return trigger.getClassName() + "#" + trigger.getMethodName();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String normalizeMethodHopToEntryKey(String hop) {
|
||||
if (hop == null || hop.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
int lastDot = hop.lastIndexOf('.');
|
||||
if (lastDot <= 0 || lastDot >= hop.length() - 1) {
|
||||
return hop;
|
||||
}
|
||||
return hop.substring(0, lastDot) + "#" + hop.substring(lastDot + 1);
|
||||
}
|
||||
|
||||
private static String triggerKey(TriggerPoint trigger) {
|
||||
if (trigger == null) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder key = new StringBuilder();
|
||||
key.append(nullToEmpty(trigger.getClassName())).append('#');
|
||||
key.append(nullToEmpty(trigger.getMethodName())).append('|');
|
||||
key.append(nullToEmpty(trigger.getEvent())).append('|');
|
||||
key.append(nullToEmpty(trigger.getConstraint())).append('|');
|
||||
key.append(trigger.isExternal()).append('|');
|
||||
key.append(trigger.isAmbiguous()).append('|');
|
||||
key.append(nullToEmpty(trigger.getSourceState()));
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
private static String linkResolutionKey(LinkResolution resolution) {
|
||||
return resolution == null ? "" : resolution.name();
|
||||
}
|
||||
|
||||
private static String matchedTransitionsKey(List<MatchedTransition> matchedTransitions) {
|
||||
if (matchedTransitions == null || matchedTransitions.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return matchedTransitions.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator
|
||||
.comparing((MatchedTransition mt) -> nullToEmpty(mt.getEvent()))
|
||||
.thenComparing(mt -> nullToEmpty(mt.getSourceState()))
|
||||
.thenComparing(mt -> nullToEmpty(mt.getTargetState())))
|
||||
.map(mt -> nullToEmpty(mt.getEvent()) + ">"
|
||||
+ nullToEmpty(mt.getSourceState()) + ">"
|
||||
+ nullToEmpty(mt.getTargetState()))
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
result.getMetadata().getTriggers(), result.getName(), context, stateMachineTransitions);
|
||||
List<CallChain> scopedChains = MachineScopeFilter.filterCallChainsForMachine(
|
||||
updatedChains, result.getName(), context, stateMachineTransitions);
|
||||
scopedChains = SemanticCallChainDedup.deduplicate(scopedChains);
|
||||
List<EntryPoint> scopedEntryPoints = EntryPointScopeResolver.scopeFromCallChains(
|
||||
scopedChains, result.getMetadata().getEntryPoints());
|
||||
|
||||
@@ -431,11 +432,20 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
== MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer a concrete trigger type; otherwise adopt the machine type only when it is also
|
||||
* concrete. Non-concrete machine types ({@code String}/{@code Object}/type-vars) must not be
|
||||
* stamped onto triggers — that makes multi-machine affinity treat them as ambiguous shared
|
||||
* generics and fail-closed, wiping package-proven String-machine chains (e.g. MavenOrder JMS).
|
||||
*/
|
||||
private static String concreteOrFallback(String triggerTypeFqn, String machineTypeFqn) {
|
||||
if (SharedServiceRoutingPolicy.isConcreteMachineType(triggerTypeFqn)) {
|
||||
return triggerTypeFqn;
|
||||
}
|
||||
return machineTypeFqn;
|
||||
if (SharedServiceRoutingPolicy.isConcreteMachineType(machineTypeFqn)) {
|
||||
return machineTypeFqn;
|
||||
}
|
||||
return triggerTypeFqn;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -105,9 +105,17 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
||||
return isStringTypeOrPrimitive(smEnumType);
|
||||
}
|
||||
|
||||
// Prevent matching enum triggers to string transitions
|
||||
if (!smEvent.contains(".") && triggerEvent.contains(".")) {
|
||||
return false;
|
||||
// Trigger scraped as String.CONST while config still stores the bare literal constant.
|
||||
if (isStringTypeOrPrimitive(eventTypeFqn) && triggerEvent.contains(".") && !smEvent.contains(".")) {
|
||||
String triggerEnumType = triggerEvent.substring(0, triggerEvent.lastIndexOf('.'));
|
||||
return isStringTypeOrPrimitive(triggerEnumType);
|
||||
}
|
||||
|
||||
// Qualified trigger against bare SM event without eventTypeFqn: only when the qualifier
|
||||
// itself is a string/primitive type (not a real enum like OrderEvent.PAY ↔ PAY).
|
||||
if (eventTypeFqn == null && !smEvent.contains(".") && triggerEvent.contains(".")) {
|
||||
String triggerEnumType = triggerEvent.substring(0, triggerEvent.lastIndexOf('.'));
|
||||
return isStringTypeOrPrimitive(triggerEnumType);
|
||||
}
|
||||
|
||||
if (smEvent.contains(".")) {
|
||||
|
||||
@@ -964,6 +964,9 @@ public final class MachineEnumCanonicalizer {
|
||||
}
|
||||
|
||||
if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
|
||||
if (isStringOrPrimitiveType(enumTypeFqn)) {
|
||||
return enumTypeFqn + "." + stripped;
|
||||
}
|
||||
// Only invent enumType.CONST when CONST is a real member — otherwise JMS payload
|
||||
// labels like "PAY" become OrderState.PAY and break transition linking.
|
||||
if (context != null && !enumHasConstant(enumTypeFqn, stripped, context)) {
|
||||
|
||||
@@ -66,7 +66,31 @@ public final class CarrierConstraintSupport {
|
||||
if (carrierBindings.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return !BooleanConstraintEvaluator.isCompatibleWithKnownBindings(constraint, carrierBindings);
|
||||
String carrierConstraint = carrierOnlyConstraint(constraint, carrierBindings.keySet());
|
||||
if (carrierConstraint == null || carrierConstraint.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return !BooleanConstraintEvaluator.isCompatibleWithKnownBindings(carrierConstraint, carrierBindings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps AND-clauses whose primary identifier overlaps {@code carrierKeys}; machine-domain
|
||||
* discriminators and unrelated locals are dropped so carrier binding checks are not skewed
|
||||
* by if-else negations on other parameters.
|
||||
*/
|
||||
static String carrierOnlyConstraint(String constraint, Set<String> carrierKeys) {
|
||||
if (constraint == null || constraint.isBlank() || carrierKeys == null || carrierKeys.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
List<String> kept = new ArrayList<>();
|
||||
for (String clause : ConstraintClauses.splitAndClauses(constraint)) {
|
||||
String ident = ConstraintClauses.primaryIdent(clause);
|
||||
if (ident == null || !containsIgnoreCase(carrierKeys, ident)) {
|
||||
continue;
|
||||
}
|
||||
kept.add(clause.trim());
|
||||
}
|
||||
return joinAndTidy(kept);
|
||||
}
|
||||
|
||||
private static Set<String> carrierPrimaryIdents(String constraint) {
|
||||
|
||||
@@ -711,6 +711,65 @@ public class ConstructorAnalyzer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an inherited field supplied through {@code super(...)} to a subclass constructor parameter type.
|
||||
* Returns null when the mapping cannot be proven.
|
||||
*/
|
||||
public String resolveSubclassConstructorParameterFqnForInheritedField(
|
||||
TypeDeclaration subclassTd,
|
||||
String inheritedFieldName,
|
||||
click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer injectionAnalyzer) {
|
||||
if (subclassTd == null || inheritedFieldName == null || inheritedFieldName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
for (MethodDeclaration ctor : subclassTd.getMethods()) {
|
||||
if (!ctor.isConstructor() || ctor.getBody() == null) {
|
||||
continue;
|
||||
}
|
||||
int paramIdx = findAssignedParameterIndex(ctor, inheritedFieldName, context, new HashSet<>());
|
||||
if (paramIdx < 0 || paramIdx >= ctor.parameters().size()) {
|
||||
continue;
|
||||
}
|
||||
SingleVariableDeclaration param = (SingleVariableDeclaration) ctor.parameters().get(paramIdx);
|
||||
IVariableBinding paramBinding = param.resolveBinding();
|
||||
if (injectionAnalyzer != null && paramBinding != null) {
|
||||
String injectedBean = injectionAnalyzer.resolveInjectedBeanFqn(paramBinding);
|
||||
if (injectedBean != null && !injectedBean.isBlank()) {
|
||||
return injectedBean;
|
||||
}
|
||||
}
|
||||
if (paramBinding != null && paramBinding.getType() != null) {
|
||||
String fqn = paramBinding.getType().getErasure().getQualifiedName();
|
||||
if (fqn != null && !fqn.isBlank() && !"java.lang.Object".equals(fqn)) {
|
||||
return fqn;
|
||||
}
|
||||
}
|
||||
CompilationUnit cu = subclassTd.getRoot() instanceof CompilationUnit root ? root : null;
|
||||
String astFqn = resolveTypeToFqn(param.getType(), cu);
|
||||
if (astFqn != null && !astFqn.isBlank()) {
|
||||
return astFqn;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Visible for unit tests verifying constructor super() parameter mapping. */
|
||||
int findAssignedParameterIndexForTest(MethodDeclaration constructorMd, String fieldName) {
|
||||
return findAssignedParameterIndex(constructorMd, fieldName, context, new HashSet<>());
|
||||
}
|
||||
|
||||
private String resolveTypeToFqn(Type type, CompilationUnit cu) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
String raw = type.toString();
|
||||
if (raw.contains("<")) {
|
||||
raw = raw.substring(0, raw.indexOf('<'));
|
||||
}
|
||||
TypeDeclaration td = cu != null ? context.getTypeDeclaration(raw, cu) : context.getTypeDeclaration(raw);
|
||||
return td != null ? context.getFqn(td) : raw;
|
||||
}
|
||||
|
||||
private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, Set<MethodDeclaration> visited) {
|
||||
if (constructorMd == null || constructorMd.getBody() == null) return -1;
|
||||
if (!visited.add(constructorMd)) return -1;
|
||||
@@ -738,67 +797,31 @@ public class ConstructorAnalyzer {
|
||||
}
|
||||
@Override
|
||||
public boolean visit(ConstructorInvocation node) {
|
||||
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
|
||||
TypeDeclaration enclosingTd = findEnclosingType(node);
|
||||
if (enclosingTd != null) {
|
||||
for (MethodDeclaration otherMd : enclosingTd.getMethods()) {
|
||||
boolean matches = false;
|
||||
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
|
||||
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
|
||||
} else {
|
||||
matches = otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size();
|
||||
}
|
||||
if (matches) {
|
||||
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
|
||||
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
|
||||
Expression arg = (Expression) node.arguments().get(targetIdx);
|
||||
String argName = extractVariableName(arg);
|
||||
if (argName != null) {
|
||||
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||
if (svd.getName().getIdentifier().equals(argName)) {
|
||||
foundIdx[0] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MethodDeclaration targetCtor = selectDelegatedConstructor(
|
||||
enclosingTd.getMethods(),
|
||||
node.resolveConstructorBinding(),
|
||||
node.arguments().size(),
|
||||
constructorMd);
|
||||
mapDelegatedConstructorArgument(targetCtor, node.arguments(), fieldName, constructorMd, foundIdx, visited);
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@Override
|
||||
public boolean visit(SuperConstructorInvocation node) {
|
||||
IMethodBinding resolvedBinding = node.resolveConstructorBinding();
|
||||
TypeDeclaration enclosingTd = findEnclosingType(node);
|
||||
if (enclosingTd != null) {
|
||||
String superFqn = context.getSuperclassFqn(enclosingTd);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
if (superTd != null) {
|
||||
for (MethodDeclaration otherMd : superTd.getMethods()) {
|
||||
boolean matches = false;
|
||||
if (resolvedBinding != null && otherMd.resolveBinding() != null) {
|
||||
matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey());
|
||||
} else {
|
||||
matches = otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size();
|
||||
}
|
||||
if (matches) {
|
||||
int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited);
|
||||
if (targetIdx >= 0 && targetIdx < node.arguments().size()) {
|
||||
Expression arg = (Expression) node.arguments().get(targetIdx);
|
||||
String argName = extractVariableName(arg);
|
||||
if (argName != null) {
|
||||
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||
if (svd.getName().getIdentifier().equals(argName)) {
|
||||
foundIdx[0] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MethodDeclaration targetCtor = selectDelegatedConstructor(
|
||||
superTd.getMethods(),
|
||||
node.resolveConstructorBinding(),
|
||||
node.arguments().size(),
|
||||
null);
|
||||
mapDelegatedConstructorArgument(targetCtor, node.arguments(), fieldName, constructorMd, foundIdx, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -808,6 +831,97 @@ public class ConstructorAnalyzer {
|
||||
return foundIdx[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer JDT binding identity when available. If the binding is missing or does not map to a
|
||||
* source constructor, fall back to a unique same-arity constructor. Ambiguous same-arity
|
||||
* overloads without a binding match return null (do not guess).
|
||||
*/
|
||||
private static MethodDeclaration selectDelegatedConstructor(
|
||||
MethodDeclaration[] candidates,
|
||||
IMethodBinding resolvedBinding,
|
||||
int argumentCount,
|
||||
MethodDeclaration exclude) {
|
||||
if (candidates == null) {
|
||||
return null;
|
||||
}
|
||||
if (resolvedBinding != null) {
|
||||
for (MethodDeclaration otherMd : candidates) {
|
||||
if (!otherMd.isConstructor() || otherMd == exclude) {
|
||||
continue;
|
||||
}
|
||||
IMethodBinding otherBinding = otherMd.resolveBinding();
|
||||
if (otherBinding != null && constructorBindingsMatch(resolvedBinding, otherBinding)) {
|
||||
return otherMd;
|
||||
}
|
||||
}
|
||||
}
|
||||
MethodDeclaration arityMatch = null;
|
||||
for (MethodDeclaration otherMd : candidates) {
|
||||
if (!otherMd.isConstructor() || otherMd == exclude) {
|
||||
continue;
|
||||
}
|
||||
if (otherMd.parameters().size() != argumentCount) {
|
||||
continue;
|
||||
}
|
||||
if (arityMatch != null) {
|
||||
// Ambiguous same-arity overloads without a binding hit — do not guess.
|
||||
return null;
|
||||
}
|
||||
arityMatch = otherMd;
|
||||
}
|
||||
return arityMatch;
|
||||
}
|
||||
|
||||
private static boolean constructorBindingsMatch(IMethodBinding left, IMethodBinding right) {
|
||||
if (left.isEqualTo(right) || Objects.equals(left.getKey(), right.getKey())) {
|
||||
return true;
|
||||
}
|
||||
if (!Objects.equals(left.getName(), right.getName())) {
|
||||
return false;
|
||||
}
|
||||
ITypeBinding[] leftParams = left.getParameterTypes();
|
||||
ITypeBinding[] rightParams = right.getParameterTypes();
|
||||
if (leftParams.length != rightParams.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < leftParams.length; i++) {
|
||||
if (!leftParams[i].getErasure().isEqualTo(rightParams[i].getErasure())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ITypeBinding leftDecl = left.getDeclaringClass();
|
||||
ITypeBinding rightDecl = right.getDeclaringClass();
|
||||
return leftDecl != null && rightDecl != null && leftDecl.getErasure().isEqualTo(rightDecl.getErasure());
|
||||
}
|
||||
|
||||
private void mapDelegatedConstructorArgument(
|
||||
MethodDeclaration targetCtor,
|
||||
List<?> arguments,
|
||||
String fieldName,
|
||||
MethodDeclaration constructorMd,
|
||||
int[] foundIdx,
|
||||
Set<MethodDeclaration> visited) {
|
||||
if (targetCtor == null || arguments == null) {
|
||||
return;
|
||||
}
|
||||
int targetIdx = findAssignedParameterIndex(targetCtor, fieldName, context, visited);
|
||||
if (targetIdx < 0 || targetIdx >= arguments.size()) {
|
||||
return;
|
||||
}
|
||||
Expression arg = (Expression) arguments.get(targetIdx);
|
||||
String argName = extractVariableName(arg);
|
||||
if (argName == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < constructorMd.parameters().size(); i++) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i);
|
||||
if (svd.getName().getIdentifier().equals(argName)) {
|
||||
foundIdx[0] = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String extractVariableName(Expression expr) {
|
||||
if (expr instanceof SimpleName sn) return sn.getIdentifier();
|
||||
if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier();
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.ITypeBinding;
|
||||
import org.eclipse.jdt.core.dom.IVariableBinding;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Narrows generic superclass collaborator fields to the concrete type supplied by a subclass
|
||||
* constructor via {@code super(...)} delegation.
|
||||
*/
|
||||
public final class ConstructorInjectionTypeNarrower {
|
||||
|
||||
private ConstructorInjectionTypeNarrower() {
|
||||
}
|
||||
|
||||
public static String resolveConcreteReceiverTypeFqn(
|
||||
IVariableBinding fieldBinding,
|
||||
TypeDeclaration callSiteType,
|
||||
CodebaseContext context,
|
||||
InjectionPointAnalyzer injectionAnalyzer,
|
||||
ConstructorAnalyzer constructorAnalyzer) {
|
||||
if (fieldBinding == null
|
||||
|| !fieldBinding.isField()
|
||||
|| context == null
|
||||
|| constructorAnalyzer == null
|
||||
|| fieldBinding.getDeclaringClass() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String fieldName = fieldBinding.getName();
|
||||
String declaringFqn = fieldBinding.getDeclaringClass().getErasure().getQualifiedName();
|
||||
if (declaringFqn == null || declaringFqn.isBlank() || fieldName == null || fieldName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<String> concreteTypes = new LinkedHashSet<>();
|
||||
for (String candidateFqn : candidateSubclassFqns(callSiteType, declaringFqn, context)) {
|
||||
TypeDeclaration candidateTd = context.getTypeDeclaration(candidateFqn);
|
||||
if (candidateTd == null || context.isSubtypeOrSame(candidateFqn, declaringFqn) == false) {
|
||||
continue;
|
||||
}
|
||||
if (candidateTd.isInterface() || org.eclipse.jdt.core.dom.Modifier.isAbstract(candidateTd.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
String narrowed = constructorAnalyzer.resolveSubclassConstructorParameterFqnForInheritedField(
|
||||
candidateTd, fieldName, injectionAnalyzer);
|
||||
if (narrowed != null && !narrowed.isBlank()) {
|
||||
concreteTypes.add(narrowed);
|
||||
}
|
||||
}
|
||||
|
||||
return concreteTypes.size() == 1 ? concreteTypes.iterator().next() : null;
|
||||
}
|
||||
|
||||
private static Set<String> candidateSubclassFqns(
|
||||
TypeDeclaration callSiteType,
|
||||
String declaringFqn,
|
||||
CodebaseContext context) {
|
||||
Set<String> candidates = new LinkedHashSet<>();
|
||||
if (callSiteType != null) {
|
||||
String callSiteFqn = context.getFqn(callSiteType);
|
||||
if (callSiteFqn != null && !callSiteFqn.isBlank() && !callSiteFqn.equals(declaringFqn)) {
|
||||
candidates.add(callSiteFqn);
|
||||
}
|
||||
}
|
||||
List<String> implementations = context.getImplementations(declaringFqn);
|
||||
if (implementations != null) {
|
||||
candidates.addAll(implementations);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
public static String resolveConcreteReceiverTypeFqnFromBinding(
|
||||
IVariableBinding fieldBinding,
|
||||
ITypeBinding callSiteTypeBinding,
|
||||
CodebaseContext context,
|
||||
InjectionPointAnalyzer injectionAnalyzer,
|
||||
ConstructorAnalyzer constructorAnalyzer) {
|
||||
if (callSiteTypeBinding == null) {
|
||||
return resolveConcreteReceiverTypeFqn(
|
||||
fieldBinding, null, context, injectionAnalyzer, constructorAnalyzer);
|
||||
}
|
||||
String callSiteFqn = callSiteTypeBinding.getErasure().getQualifiedName();
|
||||
TypeDeclaration callSiteTd = context != null ? context.getTypeDeclaration(callSiteFqn) : null;
|
||||
return resolveConcreteReceiverTypeFqn(
|
||||
fieldBinding, callSiteTd, context, injectionAnalyzer, constructorAnalyzer);
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,12 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.SemanticCallChainDedup;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
@@ -83,7 +78,10 @@ public class EnrichmentService {
|
||||
if (result.getMetadata() != null && context != null && intelligence != null) {
|
||||
List<CallChain> refreshed = CallChainEnricher.buildCallChainsForMachine(result, context, intelligence);
|
||||
if (refreshed != null) {
|
||||
List<CallChain> merged = mergeCallChainsByEntryPoint(result.getMetadata().getCallChains(), refreshed);
|
||||
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
|
||||
result.getMetadata().getCallChains(),
|
||||
refreshed,
|
||||
SemanticCallChainDedup.KeepStrategy.SHORTEST);
|
||||
result.setMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||
.triggers(result.getMetadata().getTriggers())
|
||||
.entryPoints(result.getMetadata().getEntryPoints())
|
||||
@@ -95,47 +93,6 @@ public class EnrichmentService {
|
||||
relinkAfterPropertyResolution(result, context, intelligence);
|
||||
}
|
||||
|
||||
private static List<CallChain> mergeCallChainsByEntryPoint(
|
||||
List<CallChain> existing,
|
||||
List<CallChain> refreshed) {
|
||||
Map<String, CallChain> byEntryPoint = new LinkedHashMap<>();
|
||||
if (existing != null) {
|
||||
for (CallChain chain : existing) {
|
||||
byEntryPoint.putIfAbsent(callChainKey(chain), chain);
|
||||
}
|
||||
}
|
||||
for (CallChain chain : refreshed) {
|
||||
byEntryPoint.put(callChainKey(chain), chain);
|
||||
}
|
||||
return new ArrayList<>(byEntryPoint.values());
|
||||
}
|
||||
|
||||
private static String callChainKey(CallChain chain) {
|
||||
if (chain == null) {
|
||||
return "";
|
||||
}
|
||||
String entryPointKey = entryPointKey(chain.getEntryPoint());
|
||||
if (!entryPointKey.isBlank()) {
|
||||
return entryPointKey;
|
||||
}
|
||||
if (chain.getTriggerPoint() != null
|
||||
&& chain.getTriggerPoint().getClassName() != null
|
||||
&& chain.getTriggerPoint().getMethodName() != null) {
|
||||
return chain.getTriggerPoint().getClassName() + "#" + chain.getTriggerPoint().getMethodName();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String entryPointKey(EntryPoint entryPoint) {
|
||||
if (entryPoint == null) {
|
||||
return "";
|
||||
}
|
||||
if (entryPoint.getName() != null && !entryPoint.getName().isBlank()) {
|
||||
return entryPoint.getName();
|
||||
}
|
||||
return entryPoint.getClassName() + "#" + entryPoint.getMethodName();
|
||||
}
|
||||
|
||||
private static void runEnrichers(
|
||||
List<AnalysisEnricher> enrichers,
|
||||
AnalysisResult result,
|
||||
|
||||
@@ -79,6 +79,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
log.debug("CALLGRAPH RESOLVING BEAN FOR BINDING: {} calling {}", varBinding.getName(), methodName);
|
||||
}
|
||||
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||
if (concreteFqn == null) {
|
||||
concreteFqn = ConstructorInjectionTypeNarrower.resolveConcreteReceiverTypeFqn(
|
||||
varBinding,
|
||||
enclosingType,
|
||||
context,
|
||||
injectionAnalyzer,
|
||||
constructorAnalyzer);
|
||||
}
|
||||
if (concreteFqn != null) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
|
||||
@@ -153,7 +161,17 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (!(nameBinding instanceof IVariableBinding varBinding)) {
|
||||
return null;
|
||||
}
|
||||
return injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||
String direct = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
TypeDeclaration enclosingType = findEnclosingType(receiver);
|
||||
return ConstructorInjectionTypeNarrower.resolveConcreteReceiverTypeFqn(
|
||||
varBinding,
|
||||
enclosingType,
|
||||
context,
|
||||
injectionAnalyzer,
|
||||
constructorAnalyzer);
|
||||
}
|
||||
|
||||
private Expression unwrapMessageBuilder(Expression expr) {
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SemanticCallChainDedupTest {
|
||||
|
||||
@Test
|
||||
void shouldPreferEntryAnchoredPathOverShorterOrphanPath() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.JMS)
|
||||
.name("JMS: order.queue")
|
||||
.className("com.example.JmsOrderListener")
|
||||
.methodName("onMessage")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("ORDER_EVENT")
|
||||
.build();
|
||||
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
|
||||
.event("ORDER_EVENT")
|
||||
.sourceState("BUSY")
|
||||
.targetState("DONE")
|
||||
.build());
|
||||
|
||||
CallChain anchoredLonger = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of(
|
||||
"com.example.JmsOrderListener.onMessage",
|
||||
"com.example.OrderService.onMessage",
|
||||
"com.example.StateMachine.sendEvent"))
|
||||
.build();
|
||||
CallChain orphanShorter = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("com.example.OrderService.onMessage"))
|
||||
.build();
|
||||
|
||||
List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(orphanShorter, anchoredLonger));
|
||||
|
||||
assertThat(deduped).hasSize(1);
|
||||
assertThat(deduped.get(0).getMethodChain())
|
||||
.containsExactly(
|
||||
"com.example.JmsOrderListener.onMessage",
|
||||
"com.example.OrderService.onMessage",
|
||||
"com.example.StateMachine.sendEvent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCollapseChainsWithSameSemanticKeyButDifferentPaths() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.REST)
|
||||
.name("POST /api/order/pay")
|
||||
.className("com.example.Api")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("PAY")
|
||||
.build();
|
||||
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
|
||||
.event("PAY")
|
||||
.sourceState("NEW")
|
||||
.targetState("PAID")
|
||||
.build());
|
||||
|
||||
CallChain longPath = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("a", "b", "c", "d"))
|
||||
.build();
|
||||
CallChain shortPath = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("a", "b"))
|
||||
.build();
|
||||
|
||||
List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(longPath, shortPath));
|
||||
|
||||
assertThat(deduped).hasSize(1);
|
||||
assertThat(deduped.get(0).getMethodChain()).containsExactly("a", "b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepDistinctChainsWhenMatchedTransitionsDiffer() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.name("POST /api/order")
|
||||
.className("com.example.Api")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("PAY")
|
||||
.build();
|
||||
|
||||
CallChain payChain = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(List.of(MatchedTransition.builder()
|
||||
.event("PAY")
|
||||
.sourceState("NEW")
|
||||
.targetState("PAID")
|
||||
.build()))
|
||||
.methodChain(List.of("hop1"))
|
||||
.build();
|
||||
CallChain shipChain = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(List.of(MatchedTransition.builder()
|
||||
.event("SHIP")
|
||||
.sourceState("PAID")
|
||||
.targetState("SHIPPED")
|
||||
.build()))
|
||||
.methodChain(List.of("hop1"))
|
||||
.build();
|
||||
|
||||
List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(payChain, shipChain));
|
||||
|
||||
assertThat(deduped).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstStrategyShouldKeepFirstSeenChain() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.name("JMS: orders")
|
||||
.className("com.example.Listener")
|
||||
.methodName("onMessage")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("PAY")
|
||||
.build();
|
||||
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
|
||||
.event("PAY")
|
||||
.sourceState("NEW")
|
||||
.targetState("PAID")
|
||||
.build());
|
||||
|
||||
CallChain first = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("long", "path"))
|
||||
.build();
|
||||
CallChain second = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("short"))
|
||||
.build();
|
||||
|
||||
List<CallChain> deduped = SemanticCallChainDedup.deduplicate(
|
||||
List.of(first, second),
|
||||
SemanticCallChainDedup.KeepStrategy.FIRST);
|
||||
|
||||
assertThat(deduped).hasSize(1);
|
||||
assertThat(deduped.get(0).getMethodChain()).containsExactly("long", "path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeRefreshShouldReplaceExistingChainForSameEntryPoint() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
CallChain existing = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.web.OrderStateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build())
|
||||
.methodChain(null)
|
||||
.build();
|
||||
CallChain refreshed = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.event("PAY")
|
||||
.build())
|
||||
.methodChain(List.of("com.example.web.OrderController.pay"))
|
||||
.build();
|
||||
|
||||
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
|
||||
List.of(existing),
|
||||
List.of(refreshed),
|
||||
SemanticCallChainDedup.KeepStrategy.SHORTEST);
|
||||
|
||||
assertThat(merged).hasSize(1);
|
||||
assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
|
||||
assertThat(merged.get(0).getTriggerPoint().getEvent()).isEqualTo("PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeRefreshShouldReplaceSameEntryAndTriggerEvent() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
CallChain existing = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.web.OrderStateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("PAY")
|
||||
.build())
|
||||
.methodChain(null)
|
||||
.build();
|
||||
CallChain refreshed = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.event("PAY")
|
||||
.build())
|
||||
.methodChain(List.of("com.example.web.OrderController.pay"))
|
||||
.build();
|
||||
|
||||
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
|
||||
List.of(existing),
|
||||
List.of(refreshed),
|
||||
SemanticCallChainDedup.KeepStrategy.SHORTEST);
|
||||
|
||||
assertThat(merged).hasSize(1);
|
||||
assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
|
||||
assertThat(merged.get(0).getTriggerPoint().getMethodName()).isEqualTo("pay");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeRefreshShouldKeepMultiEventFanOutFromSameEntryPoint() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.name("POST /api/order")
|
||||
.className("com.example.Api")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
CallChain existingPay = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("PAY")
|
||||
.build())
|
||||
.methodChain(List.of("com.example.Api.handle", "pay"))
|
||||
.build();
|
||||
CallChain refreshedShip = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("SHIP")
|
||||
.build())
|
||||
.methodChain(List.of("com.example.Api.handle", "ship"))
|
||||
.build();
|
||||
CallChain refreshedPay = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("PAY")
|
||||
.build())
|
||||
.methodChain(List.of("com.example.Api.handle"))
|
||||
.build();
|
||||
|
||||
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
|
||||
List.of(existingPay),
|
||||
List.of(refreshedShip, refreshedPay),
|
||||
SemanticCallChainDedup.KeepStrategy.SHORTEST);
|
||||
|
||||
assertThat(merged).hasSize(2);
|
||||
assertThat(merged)
|
||||
.anySatisfy(chain -> {
|
||||
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("PAY");
|
||||
assertThat(chain.getMethodChain()).containsExactly("com.example.Api.handle");
|
||||
});
|
||||
assertThat(merged)
|
||||
.anySatisfy(chain -> assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("SHIP"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeShouldCollapseJsonStubChainWithRefreshedEntryPoint() {
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.event("PAY")
|
||||
.sourceState("NEW")
|
||||
.build();
|
||||
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
|
||||
.event("PAY")
|
||||
.sourceState("NEW")
|
||||
.targetState("PAID")
|
||||
.build());
|
||||
|
||||
CallChain jsonStub = CallChain.builder()
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.build();
|
||||
CallChain refreshed = CallChain.builder()
|
||||
.entryPoint(EntryPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.build())
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("com.example.web.OrderController.pay"))
|
||||
.build();
|
||||
|
||||
List<CallChain> merged = SemanticCallChainDedup.merge(
|
||||
List.of(jsonStub),
|
||||
List.of(refreshed),
|
||||
SemanticCallChainDedup.KeepStrategy.SHORTEST);
|
||||
|
||||
assertThat(merged).hasSize(1);
|
||||
assertThat(merged.get(0).getEntryPoint()).isNotNull();
|
||||
assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeShouldSemanticDedupAcrossExistingAndRefreshedLists() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.name("POST /api/order/pay")
|
||||
.className("com.example.Api")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("PAY")
|
||||
.build();
|
||||
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
|
||||
.event("PAY")
|
||||
.sourceState("NEW")
|
||||
.targetState("PAID")
|
||||
.build());
|
||||
|
||||
CallChain existing = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("a", "b", "c"))
|
||||
.build();
|
||||
CallChain refreshed = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("a"))
|
||||
.build();
|
||||
|
||||
List<CallChain> merged = SemanticCallChainDedup.merge(
|
||||
List.of(existing),
|
||||
List.of(refreshed),
|
||||
SemanticCallChainDedup.KeepStrategy.SHORTEST);
|
||||
|
||||
assertThat(merged).hasSize(1);
|
||||
assertThat(merged.get(0).getMethodChain()).containsExactly("a");
|
||||
}
|
||||
}
|
||||
@@ -1028,6 +1028,102 @@ class TransitionLinkerEnricherTest {
|
||||
assertThat(resultUser.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeduplicateSemanticallyEquivalentChainsKeepingShortestPath() {
|
||||
Transition payT = new Transition();
|
||||
payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
|
||||
payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
|
||||
payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.REST)
|
||||
.name("POST /api/order/pay")
|
||||
.className("com.example.Api")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.event("OrderEvent.PAY")
|
||||
.polymorphicEvents(List.of("OrderEvent.PAY"))
|
||||
.build();
|
||||
|
||||
CallChain longPath = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.methodChain(List.of(
|
||||
"com.example.Api.pay",
|
||||
"com.example.OrderService.route",
|
||||
"com.example.OrderService.delegate",
|
||||
"com.example.StateMachine.sendEvent"))
|
||||
.build();
|
||||
CallChain shortPath = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.methodChain(List.of(
|
||||
"com.example.Api.pay",
|
||||
"com.example.StateMachine.sendEvent"))
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("OrderStateMachineConfig")
|
||||
.transitions(List.of(payT))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(longPath, shortPath)).build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(result, null, null);
|
||||
|
||||
assertThat(result.getMetadata().getCallChains()).hasSize(1);
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMethodChain())
|
||||
.containsExactly(
|
||||
"com.example.Api.pay",
|
||||
"com.example.StateMachine.sendEvent");
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotStampNonConcreteStringMachineTypeOntoTrigger() {
|
||||
Transition orderEvent = new Transition();
|
||||
orderEvent.setSourceStates(List.of(State.of("BUSY", "String.BUSY")));
|
||||
orderEvent.setTargetStates(List.of(State.of("DONE", "String.DONE")));
|
||||
orderEvent.setEvent(Event.of("ORDER_EVENT", "String.ORDER_EVENT"));
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.entryPoint(EntryPoint.builder()
|
||||
.type(EntryPoint.Type.JMS)
|
||||
.name("JMS: order.queue")
|
||||
.className("click.kamil.maven.api.JmsOrderListener")
|
||||
.methodName("onMessage")
|
||||
.build())
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.event("ORDER_EVENT")
|
||||
.polymorphicEvents(List.of("ORDER_EVENT"))
|
||||
.build())
|
||||
.methodChain(List.of(
|
||||
"click.kamil.maven.core.MavenOrderStateMachine.OrderService.onMessage"))
|
||||
.build();
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("click.kamil.maven.core.MavenOrderStateMachine")
|
||||
.eventTypeFqn("String")
|
||||
.stateTypeFqn("String")
|
||||
.transitions(List.of(orderEvent))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.entryPoints(List.of(chain.getEntryPoint()))
|
||||
.callChains(List.of(chain))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
enricher.enrich(result, null, null);
|
||||
|
||||
assertThat(result.getMetadata().getCallChains()).isNotEmpty();
|
||||
TriggerPoint linked = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||
assertThat(linked.getEventTypeFqn())
|
||||
.as("non-concrete machine String must not be stamped onto the trigger")
|
||||
.isNull();
|
||||
assertThat(linked.getStateTypeFqn()).isNull();
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getLinkResolution())
|
||||
.isEqualTo(LinkResolution.RESOLVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectChainWhenDispatcherDomainConstraintDoesNotMatchMachine() {
|
||||
Transition payT = new Transition();
|
||||
|
||||
@@ -142,6 +142,49 @@ class StrictFqnMatchingEngineTest {
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchQualifiedStringTriggerToBareConfiguredStringTransition() {
|
||||
Event smEvent = Event.of("\"INHERITED_SUBMIT\"", "INHERITED_SUBMIT");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("String.INHERITED_SUBMIT")
|
||||
.eventTypeFqn("String")
|
||||
.polymorphicEvents(List.of("String.INHERITED_SUBMIT"))
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchQualifiedStringTriggerToBareWhenEventTypeFqnMissing() {
|
||||
Event smEvent = Event.of("ORDER_EVENT", "ORDER_EVENT");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("String.ORDER_EVENT")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchEnumQualifiedTriggerToBareConstant() {
|
||||
Event smEvent = Event.of("PAY", "PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("OrderEvent.PAY")
|
||||
.eventTypeFqn("com.example.OrderEvent")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchEnumQualifiedTriggerToBareWhenEventTypeFqnMissing() {
|
||||
Event smEvent = Event.of("PAY", "PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("OrderEvent.PAY")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchCanonicalStringLiteralTriggerToConfiguredStringTransition() {
|
||||
Event smEvent = Event.of("\"AUDIT_EVENT\"", "String.AUDIT_EVENT");
|
||||
|
||||
@@ -4,6 +4,7 @@ import click.kamil.springstatemachineexporter.analysis.resolver.ConstraintClause
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -63,4 +64,30 @@ class CarrierConstraintSupportTest {
|
||||
"command == ORDER_PAY",
|
||||
Map.of("commandKey", "order.pay"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void carrierOnlyConstraintIgnoresMachineDomainNegations() {
|
||||
String constraint = "\"DOCUMENT\".equalsIgnoreCase(machineType) && \"SUBMIT\".equalsIgnoreCase(event) "
|
||||
+ "&& !(\"ORDER\".equalsIgnoreCase(machineType))";
|
||||
assertThat(CarrierConstraintSupport.carrierOnlyConstraint(constraint, Set.of("event")))
|
||||
.isEqualTo("\"SUBMIT\".equalsIgnoreCase(event)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatcherDocumentBranchDoesNotContradictEventBinding() {
|
||||
String constraint = "\"DOCUMENT\".equalsIgnoreCase(machineType) && \"SUBMIT\".equalsIgnoreCase(event) "
|
||||
+ "&& !(\"ORDER\".equalsIgnoreCase(machineType)) && \"DOCUMENT\".equalsIgnoreCase(machineType)";
|
||||
assertThat(CarrierConstraintSupport.contradictsCarrierBindings(
|
||||
constraint, Map.of("event", "SUBMIT", "eventString", "event", "machineType", "DOCUMENT")))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatcherOrderBranchDoesNotContradictEventBinding() {
|
||||
String constraint = "\"ORDER\".equalsIgnoreCase(machineType) && \"PAY\".equalsIgnoreCase(event) "
|
||||
+ "&& \"ORDER\".equalsIgnoreCase(machineType)";
|
||||
assertThat(CarrierConstraintSupport.contradictsCarrierBindings(
|
||||
constraint, Map.of("event", "PAY", "eventString", "event", "machineType", "ORDER")))
|
||||
.isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
||||
import org.eclipse.jdt.core.dom.IVariableBinding;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
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.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ConstructorInjectionTypeNarrowerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private CodebaseContext context;
|
||||
private InjectionPointAnalyzer injectionAnalyzer;
|
||||
private ConstructorAnalyzer constructorAnalyzer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
Files.writeString(tempDir.resolve("SuperInjection.java"), """
|
||||
package com.example;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
interface CollaboratorService {
|
||||
void processEvent(String event);
|
||||
}
|
||||
|
||||
@Service
|
||||
class MosService implements CollaboratorService {
|
||||
void processEvent(String event) {}
|
||||
}
|
||||
|
||||
abstract class BaseStateMachineService<T extends CollaboratorService> {
|
||||
protected final T service;
|
||||
|
||||
BaseStateMachineService(T service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
void handle(String event) {
|
||||
service.processEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
class MosStateMachineService extends BaseStateMachineService<MosService> {
|
||||
MosStateMachineService(MosService mosService) {
|
||||
super(mosService);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||
context.scan(Set.of(tempDir), Set.of());
|
||||
|
||||
SpringBeanRegistry registry = new SpringBeanRegistry();
|
||||
for (var cu : context.getCompilationUnits()) {
|
||||
cu.accept(new SpringContextScanner(registry));
|
||||
}
|
||||
injectionAnalyzer = new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
|
||||
constructorAnalyzer = new ConstructorAnalyzer(context, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIndexSubclassOfGenericBase() {
|
||||
List<String> impls = context.getImplementations("com.example.BaseStateMachineService");
|
||||
assertThat(impls).contains("com.example.MosStateMachineService");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapSuperConstructorParameterIndexForInheritedField() {
|
||||
TypeDeclaration subclass = context.getTypeDeclaration("com.example.MosStateMachineService");
|
||||
TypeDeclaration base = context.getTypeDeclaration("com.example.BaseStateMachineService");
|
||||
assertThat(context.getSuperclassFqn(subclass)).isEqualTo("com.example.BaseStateMachineService");
|
||||
|
||||
MethodDeclaration baseCtor = null;
|
||||
for (MethodDeclaration md : base.getMethods()) {
|
||||
if (md.isConstructor()) {
|
||||
baseCtor = md;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertThat(baseCtor).isNotNull();
|
||||
assertThat(constructorAnalyzer.findAssignedParameterIndexForTest(baseCtor, "service")).isEqualTo(0);
|
||||
|
||||
MethodDeclaration ctor = null;
|
||||
for (MethodDeclaration md : subclass.getMethods()) {
|
||||
if (md.isConstructor()) {
|
||||
ctor = md;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertThat(ctor).isNotNull();
|
||||
int idx = constructorAnalyzer.findAssignedParameterIndexForTest(ctor, "service");
|
||||
assertThat(idx).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapInheritedFieldToSubclassConstructorParameter() {
|
||||
TypeDeclaration subclass = context.getTypeDeclaration("com.example.MosStateMachineService");
|
||||
String narrowed = constructorAnalyzer.resolveSubclassConstructorParameterFqnForInheritedField(
|
||||
subclass, "service", injectionAnalyzer);
|
||||
assertThat(narrowed).isEqualTo("com.example.MosService");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNarrowFieldBindingInBaseMethod() {
|
||||
TypeDeclaration base = context.getTypeDeclaration("com.example.BaseStateMachineService");
|
||||
MethodDeclaration handle = context.findMethodDeclaration(base, "handle", false);
|
||||
assertThat(handle).isNotNull();
|
||||
|
||||
final IVariableBinding[] fieldBinding = {null};
|
||||
handle.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ("processEvent".equals(node.getName().getIdentifier()) && node.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
|
||||
if (sn.resolveBinding() instanceof IVariableBinding vb) {
|
||||
fieldBinding[0] = vb;
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
assertThat(fieldBinding[0]).isNotNull();
|
||||
String narrowed = ConstructorInjectionTypeNarrower.resolveConcreteReceiverTypeFqn(
|
||||
fieldBinding[0],
|
||||
base,
|
||||
context,
|
||||
injectionAnalyzer,
|
||||
constructorAnalyzer);
|
||||
assertThat(narrowed).isEqualTo("com.example.MosService");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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 java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression for generic superclass fields supplied via subclass {@code super(concreteService)}.
|
||||
*/
|
||||
class ConstructorSuperInjectionNarrowingTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private CodebaseContext context;
|
||||
private JdtCallGraphEngine engine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
writeFixture(tempDir);
|
||||
|
||||
context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||
context.scan(Set.of(tempDir), Set.of());
|
||||
|
||||
SpringBeanRegistry registry = new SpringBeanRegistry();
|
||||
for (var cu : context.getCompilationUnits()) {
|
||||
cu.accept(new SpringContextScanner(registry));
|
||||
}
|
||||
InjectionPointAnalyzer injectionAnalyzer =
|
||||
new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
|
||||
engine = new JdtCallGraphEngine(context, injectionAnalyzer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNarrowGenericSuperFieldToConcreteSubclassInjection() {
|
||||
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
|
||||
|
||||
List<CallEdge> fromShared = graph.get("com.example.BaseStateMachineService.handle");
|
||||
assertThat(fromShared)
|
||||
.as("superclass method must resolve collaborator via subclass ctor injection")
|
||||
.isNotEmpty();
|
||||
assertThat(fromShared)
|
||||
.extracting(CallEdge::getTargetMethod)
|
||||
.contains("com.example.MosService.processEvent");
|
||||
assertThat(fromShared)
|
||||
.extracting(CallEdge::getReceiverTypeFqn)
|
||||
.containsOnly("com.example.MosService");
|
||||
assertThat(fromShared)
|
||||
.extracting(CallEdge::getTargetMethod)
|
||||
.noneMatch(target -> target.contains("OtherService"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProduceSingleChainWithoutSiblingServiceFanOut() {
|
||||
List<CallChain> chains = engine.findChains(
|
||||
List.of(EntryPoint.builder()
|
||||
.type(EntryPoint.Type.CUSTOM)
|
||||
.className("com.example.MosStateMachineService")
|
||||
.methodName("dispatch")
|
||||
.build()),
|
||||
List.of(TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("MOS_EVENT")
|
||||
.build()));
|
||||
|
||||
assertThat(chains).isNotEmpty();
|
||||
assertThat(chains)
|
||||
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||
.noneMatch(hop -> hop.contains("OtherService")));
|
||||
assertThat(chains)
|
||||
.anyMatch(chain -> chain.getMethodChain().stream()
|
||||
.anyMatch(hop -> hop.contains("MosService")));
|
||||
}
|
||||
|
||||
private static void writeFixture(Path tempDir) throws Exception {
|
||||
Files.writeString(tempDir.resolve("SuperInjection.java"), """
|
||||
package com.example;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
interface CollaboratorService {
|
||||
void processEvent(String event);
|
||||
}
|
||||
|
||||
@Service
|
||||
class MosService implements CollaboratorService {
|
||||
private final StateMachine sm = new StateMachine();
|
||||
@Override
|
||||
public void processEvent(String event) {
|
||||
sm.sendEvent("MOS_EVENT");
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
class OtherService implements CollaboratorService {
|
||||
private final StateMachine sm = new StateMachine();
|
||||
@Override
|
||||
public void processEvent(String event) {
|
||||
sm.sendEvent("OTHER_EVENT");
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseStateMachineService<T extends CollaboratorService> {
|
||||
protected final T service;
|
||||
|
||||
BaseStateMachineService(T service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
void handle(String event) {
|
||||
service.processEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
class MosStateMachineService extends BaseStateMachineService<MosService> {
|
||||
MosStateMachineService(MosService mosService) {
|
||||
super(mosService);
|
||||
}
|
||||
|
||||
void dispatch(String event) {
|
||||
handle(event);
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
void sendEvent(Object event) {}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreferBindingOverSameAritySuperOverload() throws Exception {
|
||||
Path overloadDir = tempDir.resolve("overload");
|
||||
Files.createDirectories(overloadDir);
|
||||
Files.writeString(overloadDir.resolve("OverloadSuper.java"), """
|
||||
package com.example.overload;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
interface CollaboratorService {
|
||||
void processEvent(String event);
|
||||
}
|
||||
|
||||
@Service
|
||||
class MosService implements CollaboratorService {
|
||||
private final StateMachine sm = new StateMachine();
|
||||
@Override
|
||||
public void processEvent(String event) {
|
||||
sm.sendEvent("MOS_EVENT");
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
class OtherService implements CollaboratorService {
|
||||
private final StateMachine sm = new StateMachine();
|
||||
@Override
|
||||
public void processEvent(String event) {
|
||||
sm.sendEvent("OTHER_EVENT");
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseStateMachineService {
|
||||
protected final CollaboratorService service;
|
||||
|
||||
// Same arity; service is param 0 here.
|
||||
BaseStateMachineService(CollaboratorService service, String unused) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
// Same arity; service is param 1 here — must not win when binding resolves to the other ctor.
|
||||
BaseStateMachineService(String unused, CollaboratorService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
void handle(String event) {
|
||||
service.processEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
class MosStateMachineService extends BaseStateMachineService {
|
||||
MosStateMachineService(MosService mosService, String label) {
|
||||
super(mosService, label);
|
||||
}
|
||||
|
||||
void dispatch(String event) {
|
||||
handle(event);
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
void sendEvent(Object event) {}
|
||||
}
|
||||
""");
|
||||
|
||||
CodebaseContext overloadContext = new CodebaseContext();
|
||||
overloadContext.setResolveBindings(true);
|
||||
overloadContext.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||
overloadContext.scan(Set.of(overloadDir), Set.of());
|
||||
|
||||
SpringBeanRegistry registry = new SpringBeanRegistry();
|
||||
for (var cu : overloadContext.getCompilationUnits()) {
|
||||
cu.accept(new SpringContextScanner(registry));
|
||||
}
|
||||
InjectionPointAnalyzer injectionAnalyzer =
|
||||
new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
|
||||
JdtCallGraphEngine overloadEngine = new JdtCallGraphEngine(overloadContext, injectionAnalyzer);
|
||||
|
||||
Map<String, List<CallEdge>> graph = overloadEngine.buildCallGraph();
|
||||
List<CallEdge> fromShared = graph.get("com.example.overload.BaseStateMachineService.handle");
|
||||
assertThat(fromShared)
|
||||
.extracting(CallEdge::getReceiverTypeFqn)
|
||||
.containsOnly("com.example.overload.MosService");
|
||||
assertThat(fromShared)
|
||||
.extracting(CallEdge::getTargetMethod)
|
||||
.noneMatch(target -> target.contains("OtherService"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.SemanticCallChainDedup;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class EnrichmentServiceSemanticMergeTest {
|
||||
|
||||
@Test
|
||||
void refreshMergeShouldUseSemanticDedupNotEntryPointOnly() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.name("POST /api/order/pay")
|
||||
.className("com.example.Api")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.event("PAY")
|
||||
.build();
|
||||
List<MatchedTransition> matched = List.of(MatchedTransition.builder()
|
||||
.event("PAY")
|
||||
.sourceState("NEW")
|
||||
.targetState("PAID")
|
||||
.build());
|
||||
|
||||
CallChain existing = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("a", "b", "c"))
|
||||
.build();
|
||||
CallChain refreshed = CallChain.builder()
|
||||
.entryPoint(entryPoint)
|
||||
.triggerPoint(trigger)
|
||||
.linkResolution(LinkResolution.RESOLVED)
|
||||
.matchedTransitions(matched)
|
||||
.methodChain(List.of("a"))
|
||||
.build();
|
||||
|
||||
List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
|
||||
List.of(existing),
|
||||
List.of(refreshed),
|
||||
SemanticCallChainDedup.KeepStrategy.SHORTEST);
|
||||
|
||||
assertThat(merged).hasSize(1);
|
||||
assertThat(merged.get(0).getMethodChain()).containsExactly("a");
|
||||
|
||||
Transition payT = new Transition();
|
||||
payT.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||
payT.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||
payT.setEvent(Event.of("PAY", "PAY"));
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("OrderStateMachineConfig")
|
||||
.transitions(List.of(payT))
|
||||
.metadata(CodebaseMetadata.builder().callChains(merged).build())
|
||||
.build();
|
||||
|
||||
new EnrichmentService(List.of()).relinkAfterPropertyResolution(result, null, null);
|
||||
|
||||
assertThat(result.getMetadata().getCallChains()).hasSize(1);
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMethodChain()).containsExactly("a");
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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 java.util.stream.StreamSupport;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* String-typed machines with an abstract ConfigurerAdapter base must keep package-proven
|
||||
* JMS/REST chains. Linker must not stamp non-concrete {@code String} type FQNs onto triggers
|
||||
* and then fail-closed as ambiguous shared generics.
|
||||
*/
|
||||
class MavenOrderStringMachineLinkageRegressionTest {
|
||||
|
||||
@Test
|
||||
void coreModuleExportRetainsJmsAndRestResolvedChains(@TempDir Path tempDir) throws Exception {
|
||||
Path coreModule = findProjectRoot().resolve("state_machines/maven_multi_module/core-module");
|
||||
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||
exportService.runExporter(coreModule, tempDir, List.of("json"), true, List.of(), null, null,
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
||||
|
||||
JsonNode json = readMachineJson(tempDir, "MavenOrderStateMachine", new ObjectMapper());
|
||||
JsonNode chains = json.path("metadata").path("callChains");
|
||||
JsonNode entryPoints = json.path("metadata").path("entryPoints");
|
||||
|
||||
assertThat(chains.isArray()).isTrue();
|
||||
assertThat(chains.size()).isGreaterThanOrEqualTo(2);
|
||||
assertThat(entryPoints.isArray()).isTrue();
|
||||
assertThat(entryPoints.size()).isGreaterThanOrEqualTo(2);
|
||||
|
||||
JsonNode jms = findChainByEndpoint(json, "JMS: order.queue");
|
||||
assertThat(jms.path("linkResolution").asText()).isEqualTo("RESOLVED");
|
||||
assertThat(jms.path("triggerPoint").path("event").asText()).contains("ORDER_EVENT");
|
||||
assertThat(jms.path("matchedTransitions")).isNotEmpty();
|
||||
|
||||
JsonNode rest = findChainByEndpoint(json, "POST /maven/orders/submit");
|
||||
assertThat(rest.path("linkResolution").asText()).isEqualTo("RESOLVED");
|
||||
assertThat(rest.path("triggerPoint").path("event").asText()).contains("SUBMIT");
|
||||
assertThat(rest.path("matchedTransitions")).isNotEmpty();
|
||||
|
||||
assertThat(StreamSupport.stream(entryPoints.spliterator(), false)
|
||||
.anyMatch(ep -> "JMS".equals(ep.path("type").asText()))).isTrue();
|
||||
}
|
||||
|
||||
private static JsonNode findChainByEndpoint(JsonNode machineJson, String endpointName) {
|
||||
return StreamSupport.stream(machineJson.path("metadata").path("callChains").spliterator(), false)
|
||||
.filter(chain -> endpointName.equals(chain.path("entryPoint").path("name").asText()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("chain not found for " + endpointName));
|
||||
}
|
||||
|
||||
private static JsonNode readMachineJson(Path outputDir, String configBaseName, ObjectMapper mapper)
|
||||
throws Exception {
|
||||
Path machineDir;
|
||||
try (var stream = Files.list(outputDir)) {
|
||||
machineDir = stream
|
||||
.filter(Files::isDirectory)
|
||||
.filter(path -> path.getFileName().toString().endsWith(configBaseName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("No output for " + configBaseName));
|
||||
}
|
||||
return mapper.readTree(machineDir.resolve(machineDir.getFileName() + ".json").toFile());
|
||||
}
|
||||
|
||||
private static Path findProjectRoot() {
|
||||
Path current = Path.of(".").toAbsolutePath();
|
||||
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
|
||||
current = current.getParent();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class PathResolutionOptimizationTest {
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getPathBindings())
|
||||
.containsEntry("command", "DomainCommand.ORDER_PAY");
|
||||
.containsEntry("command", "com.example.DomainCommand.ORDER_PAY");
|
||||
assertThat(chains.get(0).getMethodChain()).contains("com.example.CentralDispatcher.orderPay");
|
||||
}
|
||||
|
||||
|
||||
@@ -358,15 +358,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.placeOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.place" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.PLACE_ORDER",
|
||||
"event" : "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" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 16,
|
||||
"polymorphicEvents" : [ "java.lang.String.PLACE_ORDER" ],
|
||||
"polymorphicEvents" : [ "String.PLACE_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -397,15 +397,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.OrderController.cancelOrder", "click.kamil.examples.enterprise.service.OrderServiceImpl.cancel" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.CANCEL_ORDER",
|
||||
"event" : "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" : "java.lang.String.PAID",
|
||||
"sourceState" : "String.PAID",
|
||||
"lineNumber" : 21,
|
||||
"polymorphicEvents" : [ "java.lang.String.CANCEL_ORDER" ],
|
||||
"polymorphicEvents" : [ "String.CANCEL_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -431,7 +431,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.SecurityInterceptor.preHandle" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.AUDIT_EVENT",
|
||||
"event" : "String.AUDIT_EVENT",
|
||||
"className" : "click.kamil.examples.enterprise.api.SecurityInterceptor",
|
||||
"methodName" : "preHandle",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/enterprise/api/SecurityInterceptor.java",
|
||||
@@ -466,15 +466,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.api.ReactivePaymentController.pay", "click.kamil.examples.enterprise.service.ReactivePaymentService.processPayment" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.PAY_ORDER",
|
||||
"event" : "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" : "java.lang.String.PENDING_PAYMENT",
|
||||
"sourceState" : "String.PENDING_PAYMENT",
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : [ "java.lang.String.PAY_ORDER" ],
|
||||
"polymorphicEvents" : [ "String.PAY_ORDER" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -505,13 +505,13 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ShippingJmsListener.onShippingReady" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.SHIP_ORDER",
|
||||
"event" : "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" : "java.lang.String.PAID",
|
||||
"sourceState" : "String.PAID",
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
@@ -544,13 +544,13 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.enterprise.messaging.ReturnsRabbitListener.onReturn" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.RETURN_ORDER",
|
||||
"event" : "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" : "java.lang.String.DELIVERED",
|
||||
"sourceState" : "String.DELIVERED",
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
|
||||
@@ -189,15 +189,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.reactiveOrder", "click.kamil.examples.statemachine.extended.service.ReactiveOrderService.processReactive" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.REACTIVE_EVENT",
|
||||
"event" : "String.REACTIVE_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
||||
"methodName" : "processReactive",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ReactiveOrderService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "java.lang.String.START",
|
||||
"sourceState" : "String.START",
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : [ "java.lang.String.REACTIVE_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.REACTIVE_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
|
||||
@@ -189,15 +189,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.OrderController.reactiveOrder", "click.kamil.examples.statemachine.extended.service.ReactiveOrderService.processReactive" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.REACTIVE_EVENT",
|
||||
"event" : "String.REACTIVE_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.ReactiveOrderService",
|
||||
"methodName" : "processReactive",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/ReactiveOrderService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "java.lang.String.START",
|
||||
"sourceState" : "String.START",
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : [ "java.lang.String.REACTIVE_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.REACTIVE_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
|
||||
@@ -73,15 +73,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.inheritance.api.OrderControllerImpl.submitOrder", "click.kamil.examples.statemachine.inheritance.service.ProcessingServiceImpl.doProcess" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.INHERITED_SUBMIT",
|
||||
"event" : "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" : "java.lang.String.START",
|
||||
"sourceState" : "String.START",
|
||||
"lineNumber" : 15,
|
||||
"polymorphicEvents" : [ "java.lang.String.INHERITED_SUBMIT" ],
|
||||
"polymorphicEvents" : [ "String.INHERITED_SUBMIT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
|
||||
@@ -61,6 +61,21 @@
|
||||
"metadata" : {
|
||||
"triggers" : [ ],
|
||||
"entryPoints" : [ {
|
||||
"type" : "REST",
|
||||
"name" : "POST /maven/orders/submit",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderController",
|
||||
"methodName" : "submitOrder",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
|
||||
"metadata" : {
|
||||
"path" : "/maven/orders/submit",
|
||||
"verb" : "POST"
|
||||
},
|
||||
"parameters" : [ {
|
||||
"name" : "orderId",
|
||||
"type" : "String",
|
||||
"annotations" : [ "RequestBody" ]
|
||||
} ]
|
||||
}, {
|
||||
"type" : "JMS",
|
||||
"name" : "JMS: order.queue",
|
||||
"className" : "click.kamil.maven.api.JmsOrderListener",
|
||||
@@ -77,6 +92,45 @@
|
||||
} ]
|
||||
} ],
|
||||
"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" : "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" : "String.INIT",
|
||||
"lineNumber" : 40,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : [ {
|
||||
"sourceState" : "String.INIT",
|
||||
"targetState" : "String.BUSY",
|
||||
"event" : "String.SUBMIT"
|
||||
} ],
|
||||
"linkResolution" : "RESOLVED"
|
||||
}, {
|
||||
"entryPoint" : {
|
||||
"type" : "JMS",
|
||||
"name" : "JMS: order.queue",
|
||||
@@ -93,17 +147,17 @@
|
||||
"annotations" : [ ]
|
||||
} ]
|
||||
},
|
||||
"methodChain" : [ "click.kamil.maven.api.JmsOrderListener.onMessage", "click.kamil.maven.core.MavenOrderStateMachine.OrderService.onMessage" ],
|
||||
"methodChain" : [ "click.kamil.maven.core.MavenOrderStateMachine.OrderService.onMessage" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.ORDER_EVENT",
|
||||
"event" : "String.ORDER_EVENT",
|
||||
"className" : "click.kamil.maven.core.MavenOrderStateMachine.OrderService",
|
||||
"methodName" : "onMessage",
|
||||
"sourceFile" : "core-module/src/main/java/click/kamil/maven/core/MavenOrderStateMachine.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : "java.lang.String.BUSY",
|
||||
"sourceState" : "String.BUSY",
|
||||
"lineNumber" : 52,
|
||||
"polymorphicEvents" : [ "java.lang.String.ORDER_EVENT" ],
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
|
||||
@@ -108,13 +108,13 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.multi.core.OrderController.submitOrder" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.SUBMIT",
|
||||
"event" : "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" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 18,
|
||||
"polymorphicEvents" : null,
|
||||
"external" : false,
|
||||
|
||||
@@ -367,15 +367,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.SetterInjectionController.testSetter", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.NAMED_EVENT",
|
||||
"event" : "String.NAMED_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
||||
"methodName" : "doQuirk",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : [ "java.lang.String.NAMED_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.NAMED_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -402,15 +402,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testPrimary", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.PRIMARY_EVENT",
|
||||
"event" : "String.PRIMARY_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
||||
"methodName" : "doQuirk",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 19,
|
||||
"polymorphicEvents" : [ "java.lang.String.PRIMARY_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.PRIMARY_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -437,15 +437,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testNamed", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.NAMED_EVENT",
|
||||
"event" : "String.NAMED_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
||||
"methodName" : "doQuirk",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : [ "java.lang.String.NAMED_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.NAMED_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -472,15 +472,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testQualifier", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.QUALIFIER_EVENT",
|
||||
"event" : "String.QUALIFIER_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
||||
"methodName" : "doQuirk",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : [ "java.lang.String.QUALIFIER_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.QUALIFIER_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -507,15 +507,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.QuirkController.testFallback", "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService.doQuirk" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.PRIMARY_EVENT",
|
||||
"event" : "String.PRIMARY_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.PrimaryQuirkService",
|
||||
"methodName" : "doQuirk",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PrimaryQuirkService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 19,
|
||||
"polymorphicEvents" : [ "java.lang.String.PRIMARY_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.PRIMARY_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -542,15 +542,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.ConcreteReturnTypeController.testConcrete", "click.kamil.examples.statemachine.extended.service.QualifierQuirkService.doQuirk" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.QUALIFIER_EVENT",
|
||||
"event" : "String.QUALIFIER_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.QualifierQuirkService",
|
||||
"methodName" : "doQuirk",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/QualifierQuirkService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : [ "java.lang.String.QUALIFIER_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.QUALIFIER_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -577,15 +577,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.FieldInjectionController.testField", "click.kamil.examples.statemachine.extended.service.NamedQuirkService.doQuirk" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.NAMED_EVENT",
|
||||
"event" : "String.NAMED_EVENT",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.NamedQuirkService",
|
||||
"methodName" : "doQuirk",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/NamedQuirkService.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 17,
|
||||
"polymorphicEvents" : [ "java.lang.String.NAMED_EVENT" ],
|
||||
"polymorphicEvents" : [ "String.NAMED_EVENT" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -616,15 +616,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.processBaseEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.processPayment" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.AUTHORIZE",
|
||||
"event" : "String.AUTHORIZE",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||
"methodName" : "processPayment",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.NEW",
|
||||
"sourceState" : "String.NEW",
|
||||
"lineNumber" : 22,
|
||||
"polymorphicEvents" : [ "java.lang.String.AUTHORIZE" ],
|
||||
"polymorphicEvents" : [ "String.AUTHORIZE" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
@@ -655,15 +655,15 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.examples.statemachine.extended.web.PaymentController.capturePaymentEndpoint", "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl.capturePayment" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "java.lang.String.CAPTURE",
|
||||
"event" : "String.CAPTURE",
|
||||
"className" : "click.kamil.examples.statemachine.extended.service.PaymentServiceImpl",
|
||||
"methodName" : "capturePayment",
|
||||
"sourceFile" : "src/main/java/click/kamil/examples/statemachine/extended/service/PaymentServiceImpl.java",
|
||||
"sourceModule" : null,
|
||||
"stateMachineId" : "paymentStateMachine",
|
||||
"sourceState" : "java.lang.String.AUTHORIZED",
|
||||
"sourceState" : "String.AUTHORIZED",
|
||||
"lineNumber" : 35,
|
||||
"polymorphicEvents" : [ "java.lang.String.CAPTURE" ],
|
||||
"polymorphicEvents" : [ "String.CAPTURE" ],
|
||||
"external" : false,
|
||||
"constraint" : null,
|
||||
"ambiguous" : false
|
||||
|
||||
Reference in New Issue
Block a user