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:
@@ -62,16 +62,13 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
||||
}
|
||||
|
||||
if (polyEvents.isEmpty() && isDynamicVariable(rawTriggerEvent)) {
|
||||
if (triggerPoint.getEventTypeFqn() != null) {
|
||||
if (smEventRaw.startsWith(triggerPoint.getEventTypeFqn() + ".")) {
|
||||
return true;
|
||||
}
|
||||
if (isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn()) && !smEventRaw.contains(".")) {
|
||||
return true;
|
||||
}
|
||||
if (triggerPoint.getEventTypeFqn() != null && !isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
if (triggerPoint.getEventTypeFqn() != null && isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
|
||||
return !smEventRaw.contains(".");
|
||||
}
|
||||
return !smEventRaw.contains(".");
|
||||
}
|
||||
|
||||
return matchEventNames(rawTriggerEvent, smEventRaw, triggerPoint.getEventTypeFqn());
|
||||
|
||||
@@ -6,12 +6,15 @@ import org.eclipse.jdt.core.dom.BooleanLiteral;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
|
||||
import org.eclipse.jdt.core.dom.EnumDeclaration;
|
||||
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.FieldAccess;
|
||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||
import org.eclipse.jdt.core.dom.SimpleName;
|
||||
import org.eclipse.jdt.core.dom.SimpleType;
|
||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
||||
import org.eclipse.jdt.core.dom.ThisExpression;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
@@ -76,7 +79,7 @@ public final class EnumMemberPredicateEvaluator {
|
||||
}
|
||||
List<String> filtered = new ArrayList<>();
|
||||
for (String candidate : candidates) {
|
||||
if (satisfiesPredicates(candidate, predicates, enumDecl)) {
|
||||
if (satisfiesPredicates(candidate, predicates, enumDecl, context)) {
|
||||
filtered.add(candidate);
|
||||
}
|
||||
}
|
||||
@@ -86,16 +89,17 @@ public final class EnumMemberPredicateEvaluator {
|
||||
private static boolean satisfiesPredicates(
|
||||
String canonicalConstantFqn,
|
||||
List<PredicateCall> predicates,
|
||||
EnumDeclaration enumDecl) {
|
||||
EnumDeclaration enumDecl,
|
||||
CodebaseContext context) {
|
||||
String constantName = constantSimpleName(canonicalConstantFqn);
|
||||
if (constantName == null) {
|
||||
return false;
|
||||
}
|
||||
Map<String, Boolean> boolFields = resolveConstantBooleanFields(enumDecl, constantName);
|
||||
for (PredicateCall predicate : predicates) {
|
||||
Boolean methodResult = evaluateBooleanMethod(enumDecl, predicate.methodName(), boolFields);
|
||||
Boolean methodResult = evaluateBooleanMethod(enumDecl, predicate.methodName(), boolFields, context);
|
||||
if (methodResult == null) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
boolean expected = predicate.negated ? !methodResult : methodResult;
|
||||
if (!expected) {
|
||||
@@ -108,8 +112,12 @@ public final class EnumMemberPredicateEvaluator {
|
||||
static Boolean evaluateBooleanMethod(
|
||||
EnumDeclaration enumDecl,
|
||||
String methodName,
|
||||
Map<String, Boolean> boolFields) {
|
||||
Map<String, Boolean> boolFields,
|
||||
CodebaseContext context) {
|
||||
MethodDeclaration method = findParameterlessMethod(enumDecl, methodName);
|
||||
if (method == null && context != null) {
|
||||
method = findInterfaceParameterlessMethod(enumDecl, methodName, context);
|
||||
}
|
||||
if (method == null || method.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -118,7 +126,7 @@ public final class EnumMemberPredicateEvaluator {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (result[0] == null) {
|
||||
result[0] = evaluateBooleanExpression(node.getExpression(), boolFields);
|
||||
result[0] = evaluateBooleanExpression(node.getExpression(), boolFields, enumDecl, context);
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
@@ -179,7 +187,11 @@ public final class EnumMemberPredicateEvaluator {
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean evaluateBooleanExpression(Expression expr, Map<String, Boolean> boolFields) {
|
||||
private static Boolean evaluateBooleanExpression(
|
||||
Expression expr,
|
||||
Map<String, Boolean> boolFields,
|
||||
EnumDeclaration enumDecl,
|
||||
CodebaseContext context) {
|
||||
if (expr instanceof BooleanLiteral bl) {
|
||||
return bl.booleanValue();
|
||||
}
|
||||
@@ -191,6 +203,12 @@ public final class EnumMemberPredicateEvaluator {
|
||||
return boolFields.get(fa.getName().getIdentifier());
|
||||
}
|
||||
}
|
||||
if (expr instanceof MethodInvocation mi
|
||||
&& mi.arguments().isEmpty()
|
||||
&& enumDecl != null
|
||||
&& context != null) {
|
||||
return evaluateBooleanMethod(enumDecl, mi.getName().getIdentifier(), boolFields, context);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -198,8 +216,11 @@ public final class EnumMemberPredicateEvaluator {
|
||||
return expr instanceof BooleanLiteral bl ? bl.booleanValue() : null;
|
||||
}
|
||||
|
||||
private static MethodDeclaration findParameterlessMethod(EnumDeclaration enumDecl, String methodName) {
|
||||
for (Object bodyObj : enumDecl.bodyDeclarations()) {
|
||||
private static MethodDeclaration findParameterlessMethod(AbstractTypeDeclaration typeDecl, String methodName) {
|
||||
if (typeDecl == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object bodyObj : typeDecl.bodyDeclarations()) {
|
||||
if (bodyObj instanceof MethodDeclaration md
|
||||
&& !md.isConstructor()
|
||||
&& methodName.equals(md.getName().getIdentifier())
|
||||
@@ -210,6 +231,25 @@ public final class EnumMemberPredicateEvaluator {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MethodDeclaration findInterfaceParameterlessMethod(
|
||||
EnumDeclaration enumDecl,
|
||||
String methodName,
|
||||
CodebaseContext context) {
|
||||
CompilationUnit cu = enumDecl.getRoot() instanceof CompilationUnit root ? root : null;
|
||||
for (Object typeObj : enumDecl.superInterfaceTypes()) {
|
||||
if (!(typeObj instanceof SimpleType simpleType)) {
|
||||
continue;
|
||||
}
|
||||
String interfaceName = simpleType.getName().getFullyQualifiedName();
|
||||
TypeDeclaration interfaceDecl = context.getTypeDeclaration(interfaceName, cu);
|
||||
MethodDeclaration method = findParameterlessMethod(interfaceDecl, methodName);
|
||||
if (method != null) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MethodDeclaration findEnumConstructor(EnumDeclaration enumDecl) {
|
||||
MethodDeclaration implicit = null;
|
||||
for (Object bodyObj : enumDecl.bodyDeclarations()) {
|
||||
|
||||
@@ -179,7 +179,7 @@ public final class MachineEnumCanonicalizer {
|
||||
List<Transition> machineTransitions,
|
||||
boolean skipCanonicalization) {
|
||||
TriggerPoint expanded = skipCanonicalization
|
||||
? trigger
|
||||
? expandSymbolicOnly(trigger, machineTypes, context)
|
||||
: canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
|
||||
if (expanded == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
||||
return expanded;
|
||||
@@ -199,21 +199,77 @@ public final class MachineEnumCanonicalizer {
|
||||
}
|
||||
return expanded;
|
||||
}
|
||||
if (classifyTriggerEvent(expanded.getEvent()) != TriggerEventKind.DYNAMIC_EXPRESSION) {
|
||||
return expanded;
|
||||
if (shouldInferPolymorphicEvents(expanded, machineTransitions)) {
|
||||
List<String> machineEvents = inferPolymorphicCandidates(
|
||||
expanded.getConstraint(),
|
||||
machineTypes.eventTypeFqn(),
|
||||
machineTransitions,
|
||||
context);
|
||||
if (!machineEvents.isEmpty()) {
|
||||
return expanded.toBuilder()
|
||||
.polymorphicEvents(machineEvents)
|
||||
.ambiguous(machineEvents.size() > 1)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
List<String> machineEvents = inferPolymorphicCandidates(
|
||||
expanded.getConstraint(),
|
||||
machineTypes.eventTypeFqn(),
|
||||
machineTransitions,
|
||||
context);
|
||||
if (machineEvents.isEmpty()) {
|
||||
return expanded;
|
||||
return expanded;
|
||||
}
|
||||
|
||||
private static TriggerPoint expandSymbolicOnly(
|
||||
TriggerPoint trigger,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||
CodebaseContext context) {
|
||||
if (trigger == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
||||
return trigger;
|
||||
}
|
||||
return expanded.toBuilder()
|
||||
.polymorphicEvents(machineEvents)
|
||||
.ambiguous(machineEvents.size() > 1)
|
||||
.build();
|
||||
List<String> expanded = expandSymbolicPolymorphicEvents(
|
||||
trigger.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context);
|
||||
if (java.util.Objects.equals(expanded, trigger.getPolymorphicEvents())) {
|
||||
return trigger;
|
||||
}
|
||||
return trigger.toBuilder().polymorphicEvents(expanded).build();
|
||||
}
|
||||
|
||||
private static boolean shouldInferPolymorphicEvents(
|
||||
TriggerPoint trigger,
|
||||
List<Transition> machineTransitions) {
|
||||
if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (hasOnlySymbolicPolymorphicEvents(trigger.getPolymorphicEvents())) {
|
||||
return true;
|
||||
}
|
||||
List<String> polyEvents = trigger.getPolymorphicEvents();
|
||||
if (polyEvents != null && !polyEvents.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(trigger.getConstraint())) {
|
||||
return true;
|
||||
}
|
||||
if (trigger.isExternal() || trigger.isAmbiguous()) {
|
||||
return true;
|
||||
}
|
||||
return classifyTriggerEvent(trigger.getEvent()) == TriggerEventKind.DYNAMIC_EXPRESSION;
|
||||
}
|
||||
|
||||
private static boolean hasOnlySymbolicPolymorphicEvents(List<String> polymorphicEvents) {
|
||||
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
boolean hasSymbolic = false;
|
||||
for (String pe : polymorphicEvents) {
|
||||
if (pe == null) {
|
||||
continue;
|
||||
}
|
||||
if (pe.startsWith("<SYMBOLIC:")) {
|
||||
hasSymbolic = true;
|
||||
continue;
|
||||
}
|
||||
if (classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return hasSymbolic;
|
||||
}
|
||||
|
||||
private static List<String> inferPolymorphicCandidates(
|
||||
@@ -233,6 +289,9 @@ public final class MachineEnumCanonicalizer {
|
||||
if (!filtered.isEmpty()) {
|
||||
return filtered;
|
||||
}
|
||||
if (!transitionEvents.isEmpty()) {
|
||||
return transitionEvents;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@@ -242,9 +301,12 @@ public final class MachineEnumCanonicalizer {
|
||||
String eventTypeFqn,
|
||||
List<Transition> machineTransitions,
|
||||
CodebaseContext context) {
|
||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
|
||||
List<String> result = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
current, constraint, eventTypeFqn, context);
|
||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
|
||||
if (result.isEmpty() && !transitionEvents.isEmpty()) {
|
||||
result = new ArrayList<>(transitionEvents);
|
||||
}
|
||||
if (!looksOverBroad(result, transitionEvents, constraint)) {
|
||||
return result;
|
||||
}
|
||||
@@ -281,10 +343,18 @@ public final class MachineEnumCanonicalizer {
|
||||
if (transitionEvents.isEmpty()) {
|
||||
return current.size() > 1;
|
||||
}
|
||||
return current.size() > transitionEvents.size();
|
||||
if (current.size() > transitionEvents.size()) {
|
||||
return true;
|
||||
}
|
||||
for (String event : current) {
|
||||
if (!transitionEvents.contains(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static List<String> polymorphicEventsFromTransitions(
|
||||
public static List<String> polymorphicEventsFromTransitions(
|
||||
List<Transition> machineTransitions,
|
||||
String eventTypeFqn) {
|
||||
if (machineTransitions == null || machineTransitions.isEmpty() || eventTypeFqn == null) {
|
||||
|
||||
@@ -266,6 +266,8 @@ public final class AnalysisCanonicalFormValidator {
|
||||
}
|
||||
validateMatchedTransitionsWhenResolvable(
|
||||
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
|
||||
validateOverLinkedPolymorphicEvents(
|
||||
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
|
||||
}
|
||||
if (chain.getMatchedTransitions() != null) {
|
||||
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
|
||||
@@ -339,6 +341,74 @@ public final class AnalysisCanonicalFormValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateOverLinkedPolymorphicEvents(
|
||||
String chainPrefix,
|
||||
CallChain chain,
|
||||
TriggerPoint trigger,
|
||||
List<Transition> transitions,
|
||||
String eventTypeFqn,
|
||||
List<Violation> violations) {
|
||||
if (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
|
||||
return;
|
||||
}
|
||||
List<String> transitionEvents =
|
||||
MachineEnumCanonicalizer.polymorphicEventsFromTransitions(transitions, eventTypeFqn);
|
||||
if (transitionEvents.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> extras = new ArrayList<>();
|
||||
for (String polyEvent : trigger.getPolymorphicEvents()) {
|
||||
if (!transitionEvents.contains(polyEvent)) {
|
||||
extras.add(polyEvent);
|
||||
}
|
||||
}
|
||||
if (!extras.isEmpty()) {
|
||||
violations.add(new Violation(
|
||||
chainPrefix + ".triggerPoint.polymorphicEvents",
|
||||
String.valueOf(trigger.getPolymorphicEvents().size()),
|
||||
"subset of configured transition events; unexpected: " + extras));
|
||||
return;
|
||||
}
|
||||
int matchingTransitionCount = countMatchingConfiguredTransitions(
|
||||
transitions, trigger.getPolymorphicEvents(), eventTypeFqn);
|
||||
if (matchingTransitionCount > 0
|
||||
&& chain.getMatchedTransitions() != null
|
||||
&& chain.getMatchedTransitions().size() > matchingTransitionCount) {
|
||||
violations.add(new Violation(
|
||||
chainPrefix + ".matchedTransitions",
|
||||
String.valueOf(chain.getMatchedTransitions().size()),
|
||||
"at most " + matchingTransitionCount + " for configured transitions"));
|
||||
}
|
||||
}
|
||||
|
||||
private static int countMatchingConfiguredTransitions(
|
||||
List<Transition> transitions,
|
||||
List<String> polymorphicEvents,
|
||||
String eventTypeFqn) {
|
||||
if (transitions == null || transitions.isEmpty() || polymorphicEvents == null) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
for (Transition transition : transitions) {
|
||||
if (transition.getEvent() == null) {
|
||||
continue;
|
||||
}
|
||||
String smEvent = transition.getEvent().fullIdentifier() != null
|
||||
? transition.getEvent().fullIdentifier()
|
||||
: transition.getEvent().rawName();
|
||||
for (String polyEvent : polymorphicEvents) {
|
||||
if (eventsMatch(polyEvent, smEvent, eventTypeFqn)) {
|
||||
count++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void validateMatchedTransitionsWhenResolvable(
|
||||
String chainPrefix,
|
||||
CallChain chain,
|
||||
|
||||
@@ -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