A
This commit is contained in:
@@ -200,11 +200,15 @@ public final class CallChainLinkPolicy {
|
|||||||
}
|
}
|
||||||
List<String> poly = trigger.getPolymorphicEvents();
|
List<String> poly = trigger.getPolymorphicEvents();
|
||||||
List<String> concrete = concretePolymorphicCandidates(poly, eventTypeFqn, context);
|
List<String> concrete = concretePolymorphicCandidates(poly, eventTypeFqn, context);
|
||||||
if (concrete.size() != 1) {
|
if (concrete.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return MachineEnumCanonicalizer.polymorphicEventMatchesMachineEventType(
|
for (String pe : concrete) {
|
||||||
concrete.get(0), eventTypeFqn, context);
|
if (!MachineEnumCanonicalizer.polymorphicEventMatchesMachineEventType(pe, eventTypeFqn, context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<String> concretePolymorphicCandidates(
|
static List<String> concretePolymorphicCandidates(
|
||||||
@@ -262,12 +266,13 @@ public final class CallChainLinkPolicy {
|
|||||||
if (shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, context)) {
|
if (shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, context)) {
|
||||||
return LinkResolution.AMBIGUOUS_WIDEN;
|
return LinkResolution.AMBIGUOUS_WIDEN;
|
||||||
}
|
}
|
||||||
if (ambiguousSource) {
|
// Prefer keeping useful matches over soft source ambiguity (e.g. multi-place same event).
|
||||||
return LinkResolution.AMBIGUOUS_WIDEN;
|
|
||||||
}
|
|
||||||
if (matched != null && !matched.isEmpty()) {
|
if (matched != null && !matched.isEmpty()) {
|
||||||
return LinkResolution.RESOLVED;
|
return LinkResolution.RESOLVED;
|
||||||
}
|
}
|
||||||
|
if (ambiguousSource) {
|
||||||
|
return LinkResolution.AMBIGUOUS_WIDEN;
|
||||||
|
}
|
||||||
return LinkResolution.NO_MATCH;
|
return LinkResolution.NO_MATCH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,10 +139,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
if (crossStateTypeConflict) {
|
if (crossStateTypeConflict) {
|
||||||
ambiguousSource = true;
|
ambiguousSource = true;
|
||||||
} else if (sourcesByEvent.values().stream().anyMatch(sources -> sources.size() > 1)) {
|
} else if (sourcesByEvent.values().stream().anyMatch(sources -> sources.size() > 1)) {
|
||||||
ambiguousSource = !CallChainLinkPolicy.isEndpointNarrowPolyEvidence(
|
// Same event from multiple sources within one state enum is normal — link all.
|
||||||
tp,
|
// Do not require endpoint-narrow poly evidence (that conflates "which event?"
|
||||||
effectiveEventTypeFqn,
|
// with "which place?").
|
||||||
context);
|
ambiguousSource = false;
|
||||||
} else {
|
} else {
|
||||||
Set<String> allDistinctSources = new LinkedHashSet<>();
|
Set<String> allDistinctSources = new LinkedHashSet<>();
|
||||||
sourcesByEvent.values().forEach(allDistinctSources::addAll);
|
sourcesByEvent.values().forEach(allDistinctSources::addAll);
|
||||||
@@ -291,7 +291,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
int lastDot = stateIdentifier.lastIndexOf('.');
|
int lastDot = stateIdentifier.lastIndexOf('.');
|
||||||
if (lastDot <= 0) {
|
if (lastDot <= 0) {
|
||||||
return stateIdentifier;
|
// Bare constant (NEW) — treat as same unknown type within one machine analysis.
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
return stateIdentifier.substring(0, lastDot);
|
return stateIdentifier.substring(0, lastDot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -70,6 +72,18 @@ public final class BooleanConstraintEvaluator {
|
|||||||
* is used so {@code command == ORDER_PAY} matches {@code DomainCommand.ORDER_PAY}.
|
* is used so {@code command == ORDER_PAY} matches {@code DomainCommand.ORDER_PAY}.
|
||||||
*/
|
*/
|
||||||
public static boolean isCompatibleWithBindings(String constraint, Map<String, String> bindings) {
|
public static boolean isCompatibleWithBindings(String constraint, Map<String, String> bindings) {
|
||||||
|
return isCompatibleWithBindings(constraint, bindings, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link #isCompatibleWithBindings(String, Map)} but reconstructs truncated enum refs
|
||||||
|
* using {@code preferredEnumTypeFqn} before treating them as concrete.
|
||||||
|
*/
|
||||||
|
public static boolean isCompatibleWithBindings(
|
||||||
|
String constraint,
|
||||||
|
Map<String, String> bindings,
|
||||||
|
String preferredEnumTypeFqn,
|
||||||
|
CodebaseContext context) {
|
||||||
if (constraint == null || constraint.isBlank()) {
|
if (constraint == null || constraint.isBlank()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -84,48 +98,72 @@ public final class BooleanConstraintEvaluator {
|
|||||||
}
|
}
|
||||||
String expr = constraint;
|
String expr = constraint;
|
||||||
for (Map.Entry<String, String> entry : bindings.entrySet()) {
|
for (Map.Entry<String, String> entry : bindings.entrySet()) {
|
||||||
if (!isConcreteBindingValue(entry.getKey(), entry.getValue())) {
|
String concreteValue = concreteBindingValue(
|
||||||
|
entry.getKey(), entry.getValue(), preferredEnumTypeFqn, context);
|
||||||
|
if (concreteValue == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
expr = substituteVariableBindings(expr, entry.getKey(), entry.getValue());
|
expr = substituteVariableBindings(expr, entry.getKey(), concreteValue);
|
||||||
expr = substituteEqualsLiteralBindings(expr, entry.getKey(), entry.getValue());
|
expr = substituteEqualsLiteralBindings(expr, entry.getKey(), concreteValue);
|
||||||
}
|
}
|
||||||
return evaluateBooleanExpression(expr);
|
return evaluateBooleanExpression(expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isConcreteBindingValue(String paramName, String boundValue) {
|
private static boolean isConcreteBindingValue(String paramName, String boundValue) {
|
||||||
|
return concreteBindingValue(paramName, boundValue, null, null) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return concrete value to substitute (possibly reconstructed), or {@code null} if not concrete
|
||||||
|
*/
|
||||||
|
private static String concreteBindingValue(
|
||||||
|
String paramName,
|
||||||
|
String boundValue,
|
||||||
|
String preferredEnumTypeFqn,
|
||||||
|
CodebaseContext context) {
|
||||||
if (boundValue == null || boundValue.isBlank() || boundValue.equals(paramName)) {
|
if (boundValue == null || boundValue.isBlank() || boundValue.equals(paramName)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
if (isIncompleteDottedName(boundValue)) {
|
if (isIncompleteDottedName(boundValue)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
String candidate = boundValue;
|
||||||
if (TruncatedEnumRefReconstructor.looksLikePackageTruncatedEnumRef(boundValue)) {
|
if (TruncatedEnumRefReconstructor.looksLikePackageTruncatedEnumRef(boundValue)) {
|
||||||
return false;
|
if (context == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
candidate = TruncatedEnumRefReconstructor.reconstruct(
|
||||||
|
boundValue, preferredEnumTypeFqn, context);
|
||||||
|
if (TruncatedEnumRefReconstructor.looksLikePackageTruncatedEnumRef(candidate)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (boundValue.contains("(") && !boundValue.contains("valueOf")) {
|
if (candidate.contains("(") && !candidate.contains("valueOf")) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
if (boundValue.startsWith("\"") && boundValue.endsWith("\"")) {
|
if (candidate.startsWith("\"") && candidate.endsWith("\"")) {
|
||||||
return true;
|
return candidate;
|
||||||
}
|
}
|
||||||
if ("true".equalsIgnoreCase(boundValue) || "false".equalsIgnoreCase(boundValue)) {
|
if ("true".equalsIgnoreCase(candidate) || "false".equalsIgnoreCase(candidate)) {
|
||||||
return true;
|
return candidate;
|
||||||
}
|
}
|
||||||
int lastDot = boundValue.lastIndexOf('.');
|
int lastDot = candidate.lastIndexOf('.');
|
||||||
if (lastDot >= 0
|
if (lastDot >= 0
|
||||||
&& lastDot + 1 < boundValue.length()
|
&& lastDot + 1 < candidate.length()
|
||||||
&& Character.isUpperCase(boundValue.charAt(lastDot + 1))) {
|
&& Character.isUpperCase(candidate.charAt(lastDot + 1))) {
|
||||||
return true;
|
return candidate;
|
||||||
}
|
}
|
||||||
// Treat simple routing keys (e.g. order.pay) as concrete string bindings, but avoid
|
// Treat simple routing keys (e.g. order.pay) as concrete string bindings, but avoid
|
||||||
// mistaking variable names (e.g. machineType) for concrete values.
|
// mistaking variable names (e.g. machineType) for concrete values.
|
||||||
if ((boundValue.contains(".") || boundValue.contains("/") || boundValue.contains("-"))
|
if ((candidate.contains(".") || candidate.contains("/") || candidate.contains("-"))
|
||||||
&& boundValue.matches("^[a-zA-Z0-9._/-]+$")) {
|
&& candidate.matches("^[a-zA-Z0-9._/-]+$")) {
|
||||||
return true;
|
return candidate;
|
||||||
}
|
}
|
||||||
return boundValue.equals(boundValue.toUpperCase(Locale.ROOT)) && boundValue.chars().allMatch(ch ->
|
if (candidate.equals(candidate.toUpperCase(Locale.ROOT)) && candidate.chars().allMatch(ch ->
|
||||||
Character.isUpperCase(ch) || ch == '_');
|
Character.isUpperCase(ch) || ch == '_')) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -913,6 +913,14 @@ public final class MachineEnumCanonicalizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typePart != null && context != null && context.isAmbiguousSimpleName(typePart)) {
|
if (typePart != null && context != null && context.isAmbiguousSimpleName(typePart)) {
|
||||||
|
// Prefer machine-scoped qualification when preferred enum type is known.
|
||||||
|
if (constant.matches("[A-Z_][A-Z0-9_]*")
|
||||||
|
&& (importStyleEnumTypeMatches(typePart, enumTypeFqn)
|
||||||
|
|| enumTypesMatch(enumTypeFqn, typePart, context)
|
||||||
|
|| simpleName(enumTypeFqn).equals(typePart)
|
||||||
|
|| simpleName(enumTypeFqn).equals(simpleName(typePart)))) {
|
||||||
|
return enumTypeFqn + "." + constant;
|
||||||
|
}
|
||||||
return stripped;
|
return stripped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public class PathBindingEvaluator {
|
|||||||
private final TypeResolver typeResolver;
|
private final TypeResolver typeResolver;
|
||||||
private final PathBindingExpressionResolver expressionResolver;
|
private final PathBindingExpressionResolver expressionResolver;
|
||||||
private final Map<String, Map<String, String>> traceBindingsCache = new HashMap<>();
|
private final Map<String, Map<String, String>> traceBindingsCache = new HashMap<>();
|
||||||
|
/** Optional machine/event enum FQN used to disambiguate truncated package+CONSTANT refs. */
|
||||||
|
private String preferredEnumTypeFqn;
|
||||||
|
|
||||||
public PathBindingEvaluator(
|
public PathBindingEvaluator(
|
||||||
CodebaseContext context,
|
CodebaseContext context,
|
||||||
@@ -45,6 +47,10 @@ public class PathBindingEvaluator {
|
|||||||
this.expressionResolver = expressionResolver;
|
this.expressionResolver = expressionResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPreferredEnumTypeFqn(String preferredEnumTypeFqn) {
|
||||||
|
this.preferredEnumTypeFqn = preferredEnumTypeFqn;
|
||||||
|
}
|
||||||
|
|
||||||
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
|
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
|
||||||
Map<String, String> traceBindings(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
Map<String, String> traceBindings(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
||||||
if (path == null || path.size() < 2) {
|
if (path == null || path.size() < 2) {
|
||||||
@@ -113,7 +119,8 @@ public class PathBindingEvaluator {
|
|||||||
}
|
}
|
||||||
if (edge.getConstraint() != null
|
if (edge.getConstraint() != null
|
||||||
&& !edge.getConstraint().isBlank()
|
&& !edge.getConstraint().isBlank()
|
||||||
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(edge.getConstraint(), bindings)) {
|
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(
|
||||||
|
edge.getConstraint(), bindings, preferredEnumTypeFqn, context)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
|
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
|
||||||
@@ -140,7 +147,8 @@ public class PathBindingEvaluator {
|
|||||||
Map<String, String> bindings) {
|
Map<String, String> bindings) {
|
||||||
if (edge.getConstraint() != null
|
if (edge.getConstraint() != null
|
||||||
&& !edge.getConstraint().isBlank()
|
&& !edge.getConstraint().isBlank()
|
||||||
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(edge.getConstraint(), bindings)) {
|
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(
|
||||||
|
edge.getConstraint(), bindings, preferredEnumTypeFqn, context)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (variableTracer == null || typeResolver == null) {
|
if (variableTracer == null || typeResolver == null) {
|
||||||
@@ -192,14 +200,14 @@ public class PathBindingEvaluator {
|
|||||||
if (constants.size() == 1) {
|
if (constants.size() == 1) {
|
||||||
String constant = maybeReconstruct(constants.get(0));
|
String constant = maybeReconstruct(constants.get(0));
|
||||||
if (shouldStoreBinding(paramName, constant)) {
|
if (shouldStoreBinding(paramName, constant)) {
|
||||||
bindings.put(paramName, constant);
|
bindings.put(paramName, maybeReconstruct(constant));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String resolved = resolveBindingValue(caller, arg, bindings);
|
String resolved = resolveBindingValue(caller, arg, bindings);
|
||||||
if (shouldStoreBinding(paramName, resolved)) {
|
if (shouldStoreBinding(paramName, resolved)) {
|
||||||
bindings.put(paramName, resolved);
|
bindings.put(paramName, maybeReconstruct(resolved));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,11 +219,12 @@ public class PathBindingEvaluator {
|
|||||||
if (BooleanConstraintEvaluator.isIncompleteDottedName(resolved)) {
|
if (BooleanConstraintEvaluator.isIncompleteDottedName(resolved)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Unrepaired package+CONSTANT truncation (com.example.PAY) is not a trustworthy binding.
|
// Reconstruct with preferred type before rejecting truncated package+CONSTANT refs.
|
||||||
if (TruncatedEnumRefReconstructor.looksLikePackageTruncatedEnumRef(resolved)) {
|
String candidate = maybeReconstruct(resolved);
|
||||||
|
if (TruncatedEnumRefReconstructor.looksLikePackageTruncatedEnumRef(candidate)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (resolved.contains("(") && !isEnumLikeConstant(resolved)) {
|
if (candidate.contains("(") && !isEnumLikeConstant(candidate)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -228,7 +237,7 @@ public class PathBindingEvaluator {
|
|||||||
for (Map.Entry<String, String> entry : paramValues.entrySet()) {
|
for (Map.Entry<String, String> entry : paramValues.entrySet()) {
|
||||||
String resolved = resolveBindingValue(caller, entry.getValue(), bindings);
|
String resolved = resolveBindingValue(caller, entry.getValue(), bindings);
|
||||||
if (shouldStoreBinding(entry.getKey(), resolved)) {
|
if (shouldStoreBinding(entry.getKey(), resolved)) {
|
||||||
bindings.put(entry.getKey(), resolved);
|
bindings.put(entry.getKey(), maybeReconstruct(resolved));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,7 +272,7 @@ public class PathBindingEvaluator {
|
|||||||
if (value == null || context == null) {
|
if (value == null || context == null) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return TruncatedEnumRefReconstructor.reconstruct(value, context);
|
return TruncatedEnumRefReconstructor.reconstruct(value, preferredEnumTypeFqn, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String normalizeLiteral(String value) {
|
private static String normalizeLiteral(String value) {
|
||||||
|
|||||||
@@ -71,8 +71,10 @@ class CallChainLinkPolicyTest {
|
|||||||
|
|
||||||
assertThat(CallChainLinkPolicy.isEndpointNarrowPolyEvidence(
|
assertThat(CallChainLinkPolicy.isEndpointNarrowPolyEvidence(
|
||||||
trigger, "com.example.OrderEvent")).isFalse();
|
trigger, "com.example.OrderEvent")).isFalse();
|
||||||
|
// All candidates still belong to the machine event type (trusted for routing affinity).
|
||||||
assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(
|
assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(
|
||||||
trigger, "com.example.OrderEvent")).isFalse();
|
trigger, "com.example.OrderEvent")).isTrue();
|
||||||
|
// Cross-event dynamic widen still fail-closed for linking.
|
||||||
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
|
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
|
||||||
trigger, "com.example.OrderEvent")).isTrue();
|
trigger, "com.example.OrderEvent")).isTrue();
|
||||||
}
|
}
|
||||||
@@ -130,4 +132,41 @@ class CallChainLinkPolicyTest {
|
|||||||
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
|
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
|
||||||
trigger, "com.example.order.OrderEvent")).isFalse();
|
trigger, "com.example.order.OrderEvent")).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTrustBareConstantPolymorphicCandidatesWhenMachineEventTypeIsKnown() {
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.ambiguous(true)
|
||||||
|
.event("OrderEvent.valueOf(eventStr)")
|
||||||
|
.polymorphicEvents(List.of("PAY", "SHIP"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(
|
||||||
|
trigger, "com.example.order.OrderEvent")).isTrue();
|
||||||
|
// Multiple concrete events on a dynamic trigger still fail-closed for linking.
|
||||||
|
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
|
||||||
|
trigger, "com.example.order.OrderEvent")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPreferMatchedTransitionsOverSoftAmbiguousSource() {
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.event("OrderEvent.PAY")
|
||||||
|
.polymorphicEvents(List.of("OrderEvent.PAY"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var matched = List.of(
|
||||||
|
click.kamil.springstatemachineexporter.analysis.model.MatchedTransition.builder()
|
||||||
|
.event("com.example.order.OrderEvent.PAY")
|
||||||
|
.sourceState("com.example.order.OrderState.NEW")
|
||||||
|
.build(),
|
||||||
|
click.kamil.springstatemachineexporter.analysis.model.MatchedTransition.builder()
|
||||||
|
.event("com.example.order.OrderEvent.PAY")
|
||||||
|
.sourceState("com.example.order.OrderState.PENDING")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
assertThat(CallChainLinkPolicy.resolveLinkResolution(
|
||||||
|
trigger, matched, true, "com.example.order.OrderEvent"))
|
||||||
|
.isEqualTo(LinkResolution.RESOLVED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,8 +191,10 @@ class TransitionLinkerEnricherTest {
|
|||||||
enricher.enrich(result, null, null);
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
|
||||||
assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
|
// Same event from multiple places — link all (baseline / 33ccc4a behavior).
|
||||||
assertThat(updatedChain.getTriggerPoint().isAmbiguous()).isTrue();
|
assertThat(updatedChain.getMatchedTransitions()).hasSize(2);
|
||||||
|
assertThat(updatedChain.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
|
||||||
|
assertThat(updatedChain.getTriggerPoint().isAmbiguous()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -233,11 +235,10 @@ class TransitionLinkerEnricherTest {
|
|||||||
payFromPending.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
|
payFromPending.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
|
||||||
payFromPending.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));
|
payFromPending.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));
|
||||||
|
|
||||||
|
// Known concrete event, no endpoint-narrow poly list required for multi-source linking.
|
||||||
CallChain chain = CallChain.builder()
|
CallChain chain = CallChain.builder()
|
||||||
.triggerPoint(TriggerPoint.builder()
|
.triggerPoint(TriggerPoint.builder()
|
||||||
.ambiguous(true)
|
.event("com.example.order.OrderEvent.PAY")
|
||||||
.event("OrderEvent.valueOf(eventStr)")
|
|
||||||
.polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
|
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -101,4 +102,38 @@ class BooleanConstraintEvaluatorBindingsTest {
|
|||||||
"command == ORDER_PAY",
|
"command == ORDER_PAY",
|
||||||
Map.of("command", "com.example.DomainCommand."))).isTrue();
|
Map.of("command", "com.example.DomainCommand."))).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReconstructTruncatedEnumBindingWithPreferredType(@TempDir java.nio.file.Path tempDir)
|
||||||
|
throws Exception {
|
||||||
|
java.nio.file.Path dir = tempDir.resolve("com/example");
|
||||||
|
java.nio.file.Files.createDirectories(dir);
|
||||||
|
java.nio.file.Files.writeString(dir.resolve("OrderEvent.java"),
|
||||||
|
"package com.example;\npublic enum OrderEvent { PAY, SHIP }\n");
|
||||||
|
java.nio.file.Files.writeString(dir.resolve("DocumentEvent.java"),
|
||||||
|
"package com.example;\npublic enum DocumentEvent { PAY, ARCHIVE }\n");
|
||||||
|
|
||||||
|
click.kamil.springstatemachineexporter.ast.common.CodebaseContext context =
|
||||||
|
new click.kamil.springstatemachineexporter.ast.common.CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
// Without preferred type, truncated binding is not concrete → constraint left unevaluated (true).
|
||||||
|
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
|
||||||
|
"event == PAY",
|
||||||
|
Map.of("event", "com.example.PAY"),
|
||||||
|
null,
|
||||||
|
context)).isTrue();
|
||||||
|
|
||||||
|
// With preferred type, truncated ref becomes OrderEvent.PAY and matches.
|
||||||
|
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
|
||||||
|
"event == PAY",
|
||||||
|
Map.of("event", "com.example.PAY"),
|
||||||
|
"com.example.OrderEvent",
|
||||||
|
context)).isTrue();
|
||||||
|
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
|
||||||
|
"event == SHIP",
|
||||||
|
Map.of("event", "com.example.PAY"),
|
||||||
|
"com.example.OrderEvent",
|
||||||
|
context)).isFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user