Harden polymorphic event narrowing to prevent over-linking transitions.
Fail closed when enum predicates or polymorphicEvents cannot be resolved, infer candidates from configured transitions, and validate exports that link more transitions than the machine defines. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -101,11 +101,22 @@ class StrictFqnMatchingEngineTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWildcards() {
|
||||
void shouldNotMatchWildcardAgainstEnumTransitionWithoutPolymorphicEvents() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("event").build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchDynamicMethodCallWithoutResolvedPolymorphicEvents() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("event.getPayload()")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -187,13 +198,25 @@ class StrictFqnMatchingEngineTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchDynamicVariableWithMatchingTypeFqn() {
|
||||
void shouldNotMatchDynamicVariableWithoutResolvedPolymorphicEvents() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("myCustomEvt") // Not "event", "e", etc., but a custom variable name
|
||||
.event("myCustomEvt")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchDynamicVariableWhenPolymorphicEventsAreResolved() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("myCustomEvt")
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.polymorphicEvents(List.of("com.example.OrderEvents.PAY"))
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
|
||||
@@ -209,11 +232,22 @@ class StrictFqnMatchingEngineTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchDynamicVariableWithUnknownTypeFqn() {
|
||||
void shouldNotMatchDynamicVariableWithUnknownTypeFqnWhenSmEventIsEnum() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("myCustomEvt")
|
||||
.eventTypeFqn(null) // Unknown type, fallback to true
|
||||
.eventTypeFqn(null)
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchDynamicVariableWithUnknownTypeFqnForStringEvents() {
|
||||
Event smEvent = Event.of("PAY", "PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("myCustomEvt")
|
||||
.eventTypeFqn(null)
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
@@ -250,15 +284,4 @@ class StrictFqnMatchingEngineTest {
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchDynamicMethodCallAsVariable() {
|
||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||
.event("event.getPayload()") // Dynamic method call
|
||||
.eventTypeFqn("com.example.OrderEvents")
|
||||
.build();
|
||||
|
||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,172 @@ class EnumMemberPredicatePolymorphicInferenceTest {
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNarrowEqualSizePolyListWithNonTransitionConstantsWhenConstraintIsNull(@TempDir Path tempDir) throws Exception {
|
||||
CodebaseContext context = scanEnumProject(tempDir, """
|
||||
package com.example.order;
|
||||
public enum OrderCommand {
|
||||
PAY,
|
||||
SHIP,
|
||||
META;
|
||||
}
|
||||
""");
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.event("event")
|
||||
.polymorphicEvents(List.of(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP",
|
||||
"com.example.order.OrderCommand.META"))
|
||||
.constraint(null)
|
||||
.ambiguous(true)
|
||||
.build();
|
||||
MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
|
||||
List<Transition> transitions = List.of(
|
||||
transition("OrderCommand.PAY", "NEW", "PAID"),
|
||||
transition("OrderCommand.SHIP", "PAID", "SHIPPED"));
|
||||
|
||||
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
||||
trigger, types, context, transitions, true);
|
||||
|
||||
assertThat(enriched.getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP");
|
||||
assertThat(enriched.getPolymorphicEvents())
|
||||
.doesNotContain("com.example.order.OrderCommand.META");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallBackToTransitionEventsWhenPredicateEvaluationIsInconclusive(@TempDir Path tempDir) throws Exception {
|
||||
CodebaseContext context = scanEnumProject(tempDir, """
|
||||
package com.example.order;
|
||||
public enum OrderCommand {
|
||||
PAY,
|
||||
SHIP,
|
||||
META;
|
||||
}
|
||||
""");
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.event("OrderCommand.valueOf(name)")
|
||||
.constraint("eventType.isEvent()")
|
||||
.build();
|
||||
MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
|
||||
List<Transition> transitions = List.of(
|
||||
transition("OrderCommand.PAY", "NEW", "PAID"),
|
||||
transition("OrderCommand.SHIP", "PAID", "SHIPPED"));
|
||||
|
||||
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
||||
trigger, types, context, transitions, true);
|
||||
|
||||
assertThat(enriched.getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExcludeConstantsWhenPredicateMethodCannotBeEvaluated(@TempDir Path tempDir) throws Exception {
|
||||
CodebaseContext context = scanEnumProject(tempDir, """
|
||||
package com.example.order;
|
||||
public enum OrderCommand {
|
||||
PAY,
|
||||
META;
|
||||
}
|
||||
""");
|
||||
|
||||
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
List.of(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.META"),
|
||||
"eventType.isEvent()",
|
||||
"com.example.order.OrderCommand",
|
||||
context);
|
||||
|
||||
assertThat(filtered).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEvaluateInterfaceDefaultPredicateDelegation(@TempDir Path tempDir) throws Exception {
|
||||
Path javaRoot = tempDir.resolve("src/main/java/com/example");
|
||||
Files.createDirectories(javaRoot.resolve("core"));
|
||||
Files.createDirectories(javaRoot.resolve("order"));
|
||||
Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
|
||||
Files.writeString(javaRoot.resolve("core/TriggerClassifier.java"), """
|
||||
package com.example.core;
|
||||
public interface TriggerClassifier {
|
||||
default boolean isEvent() { return isTrigger(); }
|
||||
boolean isTrigger();
|
||||
}
|
||||
""");
|
||||
Files.writeString(javaRoot.resolve("order/OrderEvent.java"), """
|
||||
package com.example.order;
|
||||
import com.example.core.TriggerClassifier;
|
||||
public enum OrderEvent implements TriggerClassifier {
|
||||
PAY(true),
|
||||
LOG(false);
|
||||
private final boolean trigger;
|
||||
OrderEvent(boolean trigger) { this.trigger = trigger; }
|
||||
@Override
|
||||
public boolean isTrigger() { return this.trigger; }
|
||||
}
|
||||
""");
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
|
||||
context.scan(tempDir);
|
||||
|
||||
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
List.of("com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent.LOG"),
|
||||
"eventType.isEvent()",
|
||||
"com.example.order.OrderEvent",
|
||||
context);
|
||||
|
||||
assertThat(filtered).containsExactly("com.example.order.OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInferPolymorphicEventsForNullPolyExternalTriggerWithConstraint(@TempDir Path tempDir) throws Exception {
|
||||
CodebaseContext context = scanEnumProject(tempDir, """
|
||||
package com.example.order;
|
||||
public enum OrderCommand {
|
||||
PAY(true),
|
||||
SHIP(true),
|
||||
META(false);
|
||||
private final boolean event;
|
||||
OrderCommand(boolean event) { this.event = event; }
|
||||
public boolean isEvent() { return event; }
|
||||
}
|
||||
""");
|
||||
|
||||
CallChain chain = CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.event("eventType")
|
||||
.constraint("eventType.isEvent()")
|
||||
.external(true)
|
||||
.ambiguous(true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Transition pay = transition("OrderCommand.PAY", "NEW", "PAID");
|
||||
Transition ship = transition("OrderCommand.SHIP", "PAID", "SHIPPED");
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.eventTypeFqn("com.example.order.OrderCommand")
|
||||
.transitions(List.of(pay, ship))
|
||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||
.build();
|
||||
|
||||
new TransitionLinkerEnricher().enrich(result, context, null);
|
||||
|
||||
TriggerPoint tp = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||
assertThat(tp.getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder(
|
||||
"com.example.order.OrderCommand.PAY",
|
||||
"com.example.order.OrderCommand.SHIP");
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchNonEventConstantsWhenPolyListWasOverBroad(@TempDir Path tempDir) throws Exception {
|
||||
CodebaseContext context = scanEnumProject(tempDir, """
|
||||
|
||||
Reference in New Issue
Block a user