diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainLinkPolicy.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainLinkPolicy.java index 88f0aa9..eed03f2 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainLinkPolicy.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainLinkPolicy.java @@ -31,9 +31,41 @@ public final class CallChainLinkPolicy { if (event != null && event.startsWith("ENUM_SET:")) { return true; } + if (event != null && event.contains(".valueOf(")) { + return MachineEnumCanonicalizer.isDynamicTriggerExpression(event) + && !isTrustedEnumPolymorphicWiden(trigger); + } return MachineEnumCanonicalizer.isDynamicTriggerExpression(event) - && event != null - && !event.contains("valueOf"); + && event != null; + } + + /** + * True when every concrete polymorphic candidate belongs to the trigger's declared event type. + * Used to allow machine-scoped symbolic expansion for generic dispatchers while rejecting + * cross-package or unqualified widens. + */ + static boolean isTrustedEnumPolymorphicWiden(TriggerPoint trigger) { + if (trigger == null) { + return false; + } + String eventTypeFqn = trigger.getEventTypeFqn(); + if (eventTypeFqn == null || eventTypeFqn.isBlank()) { + return false; + } + List poly = trigger.getPolymorphicEvents(); + if (poly == null || poly.size() <= 1) { + return false; + } + for (String pe : poly) { + if (pe == null || pe.startsWith(" { - if (e.contains(".")) return false; - String val = e; - boolean isKnownEnumVal = false; - for (List vals : context.getEnumValuesMap().values()) { - for (String v : vals) { - if (v.endsWith("." + val)) { - isKnownEnumVal = true; - break; - } - } - if (isKnownEnumVal) break; - } - if (isKnownEnumVal) return false; - return !val.equals(val.toUpperCase()) || val.length() <= 1; - }); String targetMethod = path.get(path.size() - 1); int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event, true); @@ -1086,6 +1069,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { eventParamIndex = 0; } String expectedType = typeResolver.getParameterType(targetMethod, eventParamIndex); + + polymorphicEvents.removeIf(e -> shouldDropBarePolymorphicCandidate(e, expectedType, context)); if (expectedType != null) { final String expType = expectedType; boolean isExpectedEnum = context.getEnumValues(expType) != null; @@ -2697,6 +2682,41 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { return current; } + private boolean shouldDropBarePolymorphicCandidate(String candidate, String expectedType, CodebaseContext context) { + if (candidate == null || candidate.contains(".") + || candidate.startsWith(" enumValues = context.getEnumValues(expectedType); + if (enumValues == null || enumValues.isEmpty()) { + return false; + } + for (String enumValue : enumValues) { + if (enumValue.endsWith("." + candidate)) { + return false; + } + } + return true; + } + int matchingEnumTypes = 0; + for (List enumValues : context.getEnumValuesMap().values()) { + for (String enumValue : enumValues) { + if (enumValue.endsWith("." + candidate)) { + matchingEnumTypes++; + break; + } + } + } + return matchingEnumTypes != 1; + } + private void qualifyBareEnumConstants(List polymorphicEvents, String expectedType) { if (expectedType == null || polymorphicEvents == null || polymorphicEvents.isEmpty()) { return; diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainLinkPolicyTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainLinkPolicyTest.java index 453f1cd..529c2fc 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainLinkPolicyTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainLinkPolicyTest.java @@ -33,4 +33,30 @@ class CallChainLinkPolicyTest { assertThat(CallChainLinkPolicy.resolveLinkResolution(trigger, List.of(), false)) .isEqualTo(LinkResolution.UNRESOLVED_EXTERNAL); } + + @Test + void shouldFailClosedOnValueOfAmbiguousWiden() { + TriggerPoint trigger = TriggerPoint.builder() + .ambiguous(true) + .event("OrderEvent.valueOf(eventStr)") + .polymorphicEvents(List.of("a.OrderEvent.PAY", "a.OrderEvent.SHIP")) + .build(); + + assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(trigger)).isTrue(); + assertThat(CallChainLinkPolicy.resolveLinkResolution(trigger, List.of(), false)) + .isEqualTo(LinkResolution.AMBIGUOUS_WIDEN); + } + + @Test + void shouldAllowTrustedMachineScopedValueOfWiden() { + TriggerPoint trigger = TriggerPoint.builder() + .ambiguous(true) + .eventTypeFqn("com.example.OrderEvent") + .event("OrderEvent.valueOf(eventStr)") + .polymorphicEvents(List.of("com.example.OrderEvent.PAY", "com.example.OrderEvent.SHIP")) + .build(); + + assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(trigger)).isTrue(); + assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(trigger)).isFalse(); + } }