Exclude cross-machine call chains from per-machine exports and link string-literal sendEvent triggers.
Shared String/Object generic types no longer prove machine affinity when multiple configs coexist, so Payment exports omit Order/Audit NO_MATCH stubs while @Qualifier-backed chains stay RESOLVED. String literal events now match configured string transition names for provable linking. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -22,12 +23,20 @@ public final class MachineScopeFilter {
|
||||
|
||||
public static List<TriggerPoint> filterTriggersForMachine(
|
||||
List<TriggerPoint> triggers, String machineName, CodebaseContext context) {
|
||||
return filterTriggersForMachine(triggers, machineName, context, null);
|
||||
}
|
||||
|
||||
public static List<TriggerPoint> filterTriggersForMachine(
|
||||
List<TriggerPoint> triggers,
|
||||
String machineName,
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions) {
|
||||
if (triggers == null || machineName == null) {
|
||||
return triggers == null ? List.of() : triggers;
|
||||
}
|
||||
List<TriggerPoint> filtered = new ArrayList<>();
|
||||
for (TriggerPoint trigger : triggers) {
|
||||
if (isTriggerForMachine(trigger, machineName, context)) {
|
||||
if (isTriggerForMachine(trigger, machineName, context, machineTransitions)) {
|
||||
filtered.add(trigger);
|
||||
}
|
||||
}
|
||||
@@ -36,12 +45,20 @@ public final class MachineScopeFilter {
|
||||
|
||||
public static List<CallChain> filterCallChainsForMachine(
|
||||
List<CallChain> chains, String machineName, CodebaseContext context) {
|
||||
return filterCallChainsForMachine(chains, machineName, context, null);
|
||||
}
|
||||
|
||||
public static List<CallChain> filterCallChainsForMachine(
|
||||
List<CallChain> chains,
|
||||
String machineName,
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions) {
|
||||
if (chains == null || machineName == null) {
|
||||
return chains == null ? List.of() : chains;
|
||||
}
|
||||
List<CallChain> filtered = new ArrayList<>();
|
||||
for (CallChain chain : chains) {
|
||||
if (ROUTING.isRoutedToCorrectMachine(chain, machineName, context)) {
|
||||
if (ROUTING.hasProvenMachineAffinity(chain, machineName, context, machineTransitions)) {
|
||||
filtered.add(chain);
|
||||
}
|
||||
}
|
||||
@@ -84,11 +101,12 @@ public final class MachineScopeFilter {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isTriggerForMachine(TriggerPoint trigger, String machineName, CodebaseContext context) {
|
||||
private static boolean isTriggerForMachine(
|
||||
TriggerPoint trigger, String machineName, CodebaseContext context, List<Transition> machineTransitions) {
|
||||
if (trigger == null) {
|
||||
return false;
|
||||
}
|
||||
CallChain probe = CallChain.builder().triggerPoint(trigger).methodChain(List.of()).build();
|
||||
return ROUTING.isRoutedToCorrectMachine(probe, machineName, context);
|
||||
return ROUTING.hasProvenMachineAffinity(probe, machineName, context, machineTransitions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
Map<String, String> canonicalSourceBySimplified = new LinkedHashMap<>();
|
||||
for (Transition t : stateMachineTransitions) {
|
||||
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)
|
||||
&& isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||
&& isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
String smEventForLink = canonicalEvent(t.getEvent());
|
||||
for (State smSourceState : t.getSourceStates()) {
|
||||
@@ -125,7 +125,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
.targetState(smSourceForLink)
|
||||
.event(smEventForLink)
|
||||
.build();
|
||||
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||
if (isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
matched.add(mt);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
.targetState(targetForLink)
|
||||
.event(smEventForLink)
|
||||
.build();
|
||||
if (isRoutedToCorrectMachine(chain, result.getName(), context)
|
||||
if (isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions)
|
||||
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
|
||||
matched.add(mt);
|
||||
}
|
||||
@@ -172,14 +172,16 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
|
||||
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
|
||||
result.getMetadata().getTriggers(), result.getName(), context);
|
||||
result.getMetadata().getTriggers(), result.getName(), context, stateMachineTransitions);
|
||||
List<CallChain> scopedChains = MachineScopeFilter.filterCallChainsForMachine(
|
||||
updatedChains, result.getName(), context, stateMachineTransitions);
|
||||
List<EntryPoint> scopedEntryPoints = EntryPointScopeResolver.scopeFromCallChains(
|
||||
updatedChains, result.getMetadata().getEntryPoints());
|
||||
scopedChains, result.getMetadata().getEntryPoints());
|
||||
|
||||
CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
|
||||
.triggers(scopedTriggers)
|
||||
.entryPoints(scopedEntryPoints)
|
||||
.callChains(updatedChains)
|
||||
.callChains(scopedChains)
|
||||
.properties(result.getMetadata().getProperties())
|
||||
.build();
|
||||
|
||||
@@ -187,8 +189,9 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
|
||||
|
||||
private boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||
return routingEngine.isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||
private boolean isRoutedToCorrectMachine(
|
||||
CallChain chain, String currentMachineName, CodebaseContext context, List<Transition> machineTransitions) {
|
||||
return routingEngine.hasProvenMachineAffinity(chain, currentMachineName, context, machineTransitions);
|
||||
}
|
||||
|
||||
private boolean isConstraintCompatible(String constraint, String machineName) {
|
||||
|
||||
@@ -88,9 +88,10 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
||||
|
||||
if (!tConst.equals(smConst)) return false;
|
||||
|
||||
// Prevent matching string/primitive triggers to enum transitions
|
||||
// String-typed triggers may match configured string transition events by constant name.
|
||||
if (isStringTypeOrPrimitive(eventTypeFqn) && smEvent.contains(".") && !triggerEvent.contains(".")) {
|
||||
return false;
|
||||
String smEnumType = smEvent.substring(0, smEvent.lastIndexOf('.'));
|
||||
return isStringTypeOrPrimitive(smEnumType);
|
||||
}
|
||||
|
||||
// Prevent matching enum triggers to string transitions
|
||||
|
||||
@@ -2,8 +2,19 @@ package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BeanResolutionEngine {
|
||||
boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context);
|
||||
|
||||
default boolean hasProvenMachineAffinity(
|
||||
CallChain chain,
|
||||
String currentMachineName,
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions) {
|
||||
return isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,76 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import org.eclipse.jdt.core.dom.Annotation;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
|
||||
private final StrictFqnMatchingEngine matchingEngine = new StrictFqnMatchingEngine();
|
||||
|
||||
private enum DistinctTypeAffinity {
|
||||
FOR_MACHINE,
|
||||
AGAINST_MACHINE,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasProvenMachineAffinity(
|
||||
CallChain chain,
|
||||
String currentMachineName,
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions) {
|
||||
if (chain == null) {
|
||||
return false;
|
||||
}
|
||||
if (currentMachineName == null) {
|
||||
return isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||
}
|
||||
|
||||
String explicitTarget = resolveExplicitTarget(chain);
|
||||
if (explicitTarget != null && !explicitTarget.isEmpty() && context != null) {
|
||||
Boolean namedTarget = resolveNamedBeanTarget(explicitTarget, currentMachineName, context);
|
||||
if (namedTarget != null) {
|
||||
return namedTarget;
|
||||
}
|
||||
}
|
||||
|
||||
if (chain.getTriggerPoint() != null && context != null) {
|
||||
DistinctTypeAffinity typeAffinity =
|
||||
resolveDistinctTypeAffinity(chain.getTriggerPoint(), currentMachineName, context);
|
||||
if (typeAffinity == DistinctTypeAffinity.FOR_MACHINE) {
|
||||
return true;
|
||||
}
|
||||
if (typeAffinity == DistinctTypeAffinity.AGAINST_MACHINE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (context == null || isSingleStateMachine(context)) {
|
||||
return isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||
}
|
||||
|
||||
TriggerPoint trigger = chain.getTriggerPoint();
|
||||
if (trigger != null && matchesConfiguredTransitionEvent(trigger, machineTransitions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRoutedToCorrectMachine(CallChain chain, String currentMachineName, CodebaseContext context) {
|
||||
String explicitTarget = chain.getContextMachineId();
|
||||
if (explicitTarget == null && chain.getTriggerPoint() != null) {
|
||||
explicitTarget = chain.getTriggerPoint().getStateMachineId();
|
||||
}
|
||||
String explicitTarget = resolveExplicitTarget(chain);
|
||||
if (explicitTarget != null && !explicitTarget.isEmpty() && context != null && currentMachineName != null) {
|
||||
Boolean namedTarget = resolveNamedBeanTarget(explicitTarget, currentMachineName, context);
|
||||
if (namedTarget != null) {
|
||||
@@ -59,7 +112,11 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
return true;
|
||||
if (!isAmbiguousSharedGenericMatch(
|
||||
triggerEventFqn, triggerStateFqn, machineStateFqn, machineEventFqn)
|
||||
|| isSingleStateMachine(context)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (mismatched) {
|
||||
return false;
|
||||
@@ -144,6 +201,105 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
||||
return context == null || isSingleStateMachine(context);
|
||||
}
|
||||
|
||||
private static String resolveExplicitTarget(CallChain chain) {
|
||||
if (chain == null) {
|
||||
return null;
|
||||
}
|
||||
String explicitTarget = chain.getContextMachineId();
|
||||
if (explicitTarget == null && chain.getTriggerPoint() != null) {
|
||||
explicitTarget = chain.getTriggerPoint().getStateMachineId();
|
||||
}
|
||||
return explicitTarget;
|
||||
}
|
||||
|
||||
private DistinctTypeAffinity resolveDistinctTypeAffinity(
|
||||
TriggerPoint trigger, String machineName, CodebaseContext context) {
|
||||
String triggerEventFqn = trigger.getEventTypeFqn();
|
||||
String triggerStateFqn = trigger.getStateTypeFqn();
|
||||
if (triggerEventFqn == null && triggerStateFqn == null) {
|
||||
return DistinctTypeAffinity.UNKNOWN;
|
||||
}
|
||||
|
||||
String[] machineTypes = StateMachineTypeResolver.resolve(machineName, context);
|
||||
String machineStateFqn = machineTypes[0];
|
||||
String machineEventFqn = machineTypes[1];
|
||||
|
||||
boolean matched = false;
|
||||
boolean mismatched = false;
|
||||
|
||||
if (triggerEventFqn != null && machineEventFqn != null) {
|
||||
if (typesEquivalent(triggerEventFqn, machineEventFqn)
|
||||
&& !isAmbiguousSharedGenericType(triggerEventFqn)) {
|
||||
matched = true;
|
||||
} else if (isDistinctTypeMismatched(triggerEventFqn, machineEventFqn)) {
|
||||
mismatched = true;
|
||||
}
|
||||
}
|
||||
if (triggerStateFqn != null && machineStateFqn != null) {
|
||||
if (typesEquivalent(triggerStateFqn, machineStateFqn)
|
||||
&& !isAmbiguousSharedGenericType(triggerStateFqn)) {
|
||||
matched = true;
|
||||
} else if (isDistinctTypeMismatched(triggerStateFqn, machineStateFqn)) {
|
||||
mismatched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
return DistinctTypeAffinity.FOR_MACHINE;
|
||||
}
|
||||
if (mismatched) {
|
||||
return DistinctTypeAffinity.AGAINST_MACHINE;
|
||||
}
|
||||
return DistinctTypeAffinity.UNKNOWN;
|
||||
}
|
||||
|
||||
private boolean matchesConfiguredTransitionEvent(TriggerPoint trigger, List<Transition> machineTransitions) {
|
||||
if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (Transition transition : machineTransitions) {
|
||||
if (transition.getEvent() != null && matchingEngine.matches(transition.getEvent(), trigger)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAmbiguousSharedGenericMatch(
|
||||
String triggerEventFqn,
|
||||
String triggerStateFqn,
|
||||
String machineStateFqn,
|
||||
String machineEventFqn) {
|
||||
boolean eventShared = triggerEventFqn != null
|
||||
&& machineEventFqn != null
|
||||
&& typesEquivalent(triggerEventFqn, machineEventFqn)
|
||||
&& isAmbiguousSharedGenericType(triggerEventFqn);
|
||||
boolean stateShared = triggerStateFqn != null
|
||||
&& machineStateFqn != null
|
||||
&& typesEquivalent(triggerStateFqn, machineStateFqn)
|
||||
&& isAmbiguousSharedGenericType(triggerStateFqn);
|
||||
return eventShared || stateShared;
|
||||
}
|
||||
|
||||
private boolean isAmbiguousSharedGenericType(String typeFqn) {
|
||||
return isErasedOrOpaqueType(typeFqn) || isStringType(typeFqn);
|
||||
}
|
||||
|
||||
private boolean isDistinctTypeMismatched(String type1, String type2) {
|
||||
if (isAmbiguousSharedGenericType(type1) || isAmbiguousSharedGenericType(type2)) {
|
||||
return false;
|
||||
}
|
||||
return isTypeMismatched(type1, type2);
|
||||
}
|
||||
|
||||
private boolean isStringType(String typeFqn) {
|
||||
if (typeFqn == null) {
|
||||
return false;
|
||||
}
|
||||
String erased = eraseGenerics(typeFqn);
|
||||
return "java.lang.String".equals(erased) || "String".equals(erased);
|
||||
}
|
||||
|
||||
private Boolean resolveNamedBeanTarget(String beanName, String currentMachineName, CodebaseContext context) {
|
||||
String normalized = stripAnnotationQuotes(beanName);
|
||||
if (normalized == null || normalized.isEmpty()) {
|
||||
|
||||
Reference in New Issue
Block a user