Filter polymorphic events by enum predicates and machine transitions.

Add generic constraint-aware enum member evaluation, prefer transitions[] over full enum inference, and regression tests for boolean enum dispatchers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 16:14:44 +02:00
parent 873e2c91f3
commit 77bf840465
5 changed files with 864 additions and 7 deletions

View File

@@ -0,0 +1,305 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ASTVisitor;
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.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.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Evaluates no-arg boolean member calls in dispatcher constraints (e.g. {@code eventType.isEvent()})
* against concrete enum constants by reading enum source — no hard-coded predicate names.
*/
public final class EnumMemberPredicateEvaluator {
private static final Pattern PREDICATE_PART =
Pattern.compile("!?\\s*([a-zA-Z][\\w]*)\\s*\\.\\s*([a-zA-Z][\\w]*)\\s*\\(\\s*\\)");
private EnumMemberPredicateEvaluator() {
}
public record PredicateCall(String receiverName, String methodName, boolean negated) {
}
public static boolean hasEnumMemberPredicates(String constraint) {
return !extractPredicateCalls(constraint).isEmpty();
}
public static List<PredicateCall> extractPredicateCalls(String constraint) {
if (constraint == null || constraint.isBlank()) {
return List.of();
}
List<PredicateCall> calls = new ArrayList<>();
for (String part : splitTopLevelAndParts(constraint)) {
Matcher matcher = PREDICATE_PART.matcher(part.trim());
if (matcher.matches()) {
boolean negated = part.trim().startsWith("!");
calls.add(new PredicateCall(matcher.group(1), matcher.group(2), negated));
}
}
return calls;
}
public static List<String> filterEnumConstants(
List<String> candidates,
String constraint,
String enumTypeFqn,
CodebaseContext context) {
if (candidates == null || candidates.isEmpty()) {
return candidates == null ? List.of() : candidates;
}
List<PredicateCall> predicates = extractPredicateCalls(constraint);
if (predicates.isEmpty()) {
return candidates;
}
EnumDeclaration enumDecl = findEnumDeclaration(enumTypeFqn, context);
if (enumDecl == null) {
return List.of();
}
List<String> filtered = new ArrayList<>();
for (String candidate : candidates) {
if (satisfiesPredicates(candidate, predicates, enumDecl)) {
filtered.add(candidate);
}
}
return filtered;
}
private static boolean satisfiesPredicates(
String canonicalConstantFqn,
List<PredicateCall> predicates,
EnumDeclaration enumDecl) {
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);
if (methodResult == null) {
return true;
}
boolean expected = predicate.negated ? !methodResult : methodResult;
if (!expected) {
return false;
}
}
return true;
}
static Boolean evaluateBooleanMethod(
EnumDeclaration enumDecl,
String methodName,
Map<String, Boolean> boolFields) {
MethodDeclaration method = findParameterlessMethod(enumDecl, methodName);
if (method == null || method.getBody() == null) {
return null;
}
Boolean[] result = new Boolean[1];
method.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
if (result[0] == null) {
result[0] = evaluateBooleanExpression(node.getExpression(), boolFields);
}
return super.visit(node);
}
});
return result[0];
}
static Map<String, Boolean> resolveConstantBooleanFields(EnumDeclaration enumDecl, String constantName) {
Map<String, Boolean> fields = new HashMap<>();
EnumConstantDeclaration constant = findEnumConstant(enumDecl, constantName);
if (constant == null) {
return fields;
}
MethodDeclaration constructor = findEnumConstructor(enumDecl);
if (constructor != null && constant.arguments().size() == constructor.parameters().size()) {
for (int i = 0; i < constructor.parameters().size(); i++) {
SingleVariableDeclaration param =
(SingleVariableDeclaration) constructor.parameters().get(i);
Expression arg = (Expression) constant.arguments().get(i);
Boolean boolVal = literalBoolean(arg);
if (boolVal != null) {
fields.put(param.getName().getIdentifier(), boolVal);
mapFieldName(enumDecl, param.getName().getIdentifier(), boolVal, fields);
}
}
}
for (Object bodyObj : enumDecl.bodyDeclarations()) {
if (bodyObj instanceof FieldDeclaration fd) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
Expression init = fragment.getInitializer();
Boolean boolVal = literalBoolean(init);
if (boolVal != null) {
fields.put(fragment.getName().getIdentifier(), boolVal);
}
}
}
}
}
return fields;
}
private static void mapFieldName(
EnumDeclaration enumDecl,
String paramName,
Boolean value,
Map<String, Boolean> fields) {
for (Object bodyObj : enumDecl.bodyDeclarations()) {
if (!(bodyObj instanceof FieldDeclaration fd)) {
continue;
}
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment
&& fragment.getName().getIdentifier().equals(paramName)) {
fields.put(paramName, value);
}
}
}
}
private static Boolean evaluateBooleanExpression(Expression expr, Map<String, Boolean> boolFields) {
if (expr instanceof BooleanLiteral bl) {
return bl.booleanValue();
}
if (expr instanceof SimpleName sn) {
return boolFields.get(sn.getIdentifier());
}
if (expr instanceof FieldAccess fa) {
if (fa.getExpression() == null || fa.getExpression() instanceof ThisExpression) {
return boolFields.get(fa.getName().getIdentifier());
}
}
return null;
}
private static Boolean literalBoolean(Expression expr) {
return expr instanceof BooleanLiteral bl ? bl.booleanValue() : null;
}
private static MethodDeclaration findParameterlessMethod(EnumDeclaration enumDecl, String methodName) {
for (Object bodyObj : enumDecl.bodyDeclarations()) {
if (bodyObj instanceof MethodDeclaration md
&& !md.isConstructor()
&& methodName.equals(md.getName().getIdentifier())
&& md.parameters().isEmpty()) {
return md;
}
}
return null;
}
private static MethodDeclaration findEnumConstructor(EnumDeclaration enumDecl) {
MethodDeclaration implicit = null;
for (Object bodyObj : enumDecl.bodyDeclarations()) {
if (bodyObj instanceof MethodDeclaration md && md.isConstructor()) {
if (md.parameters().isEmpty()) {
implicit = md;
} else {
return md;
}
}
}
return implicit;
}
private static EnumConstantDeclaration findEnumConstant(EnumDeclaration enumDecl, String constantName) {
for (Object constantObj : enumDecl.enumConstants()) {
if (constantObj instanceof EnumConstantDeclaration ecd
&& constantName.equals(ecd.getName().getIdentifier())) {
return ecd;
}
}
return null;
}
static EnumDeclaration findEnumDeclaration(String enumTypeFqn, CodebaseContext context) {
if (enumTypeFqn == null || context == null) {
return null;
}
String clean = enumTypeFqn.contains("<") ? enumTypeFqn.substring(0, enumTypeFqn.indexOf('<')) : enumTypeFqn;
for (CompilationUnit cu : context.getCompilationUnits()) {
String pkg = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
for (Object typeObj : cu.types()) {
EnumDeclaration found = findEnumNode(typeObj, clean, pkg, context);
if (found != null) {
return found;
}
}
}
return null;
}
private static EnumDeclaration findEnumNode(Object typeObj, String targetFqn, String pkg, CodebaseContext context) {
if (typeObj instanceof EnumDeclaration ed) {
String fqn = pkg.isEmpty() ? ed.getName().getIdentifier() : pkg + "." + ed.getName().getIdentifier();
if (targetFqn.equals(fqn)) {
return ed;
}
} else if (typeObj instanceof TypeDeclaration td) {
String parentFqn = pkg.isEmpty() ? td.getName().getIdentifier() : pkg + "." + td.getName().getIdentifier();
for (Object decl : td.bodyDeclarations()) {
if (decl instanceof EnumDeclaration nested) {
String nestedFqn = parentFqn + "." + nested.getName().getIdentifier();
if (targetFqn.equals(nestedFqn)) {
return nested;
}
}
}
}
return null;
}
private static String constantSimpleName(String canonicalConstantFqn) {
if (canonicalConstantFqn == null || !canonicalConstantFqn.contains(".")) {
return canonicalConstantFqn;
}
return canonicalConstantFqn.substring(canonicalConstantFqn.lastIndexOf('.') + 1);
}
private static List<String> splitTopLevelAndParts(String constraint) {
List<String> parts = new ArrayList<>();
StringBuilder current = new StringBuilder();
int depth = 0;
for (int i = 0; i < constraint.length(); i++) {
char c = constraint.charAt(i);
if (c == '(') {
depth++;
} else if (c == ')') {
depth--;
} else if (depth == 0 && i + 1 < constraint.length()
&& c == '&' && constraint.charAt(i + 1) == '&') {
parts.add(current.toString());
current.setLength(0);
i++;
continue;
}
current.append(c);
}
if (!current.isEmpty()) {
parts.add(current.toString());
}
return parts;
}
}

View File

@@ -185,19 +185,103 @@ public final class MachineEnumCanonicalizer {
return expanded;
}
if (hasConcretePolymorphicEvents(expanded.getPolymorphicEvents())) {
List<String> narrowed = narrowPolymorphicCandidates(
expanded.getPolymorphicEvents(),
expanded.getConstraint(),
machineTypes.eventTypeFqn(),
machineTransitions,
context);
if (!narrowed.equals(expanded.getPolymorphicEvents())) {
return expanded.toBuilder()
.polymorphicEvents(narrowed)
.ambiguous(narrowed.size() > 1)
.build();
}
return expanded;
}
if (classifyTriggerEvent(expanded.getEvent()) != TriggerEventKind.DYNAMIC_EXPRESSION) {
return expanded;
}
List<String> machineEvents = allPackageCanonicalEnumConstants(machineTypes.eventTypeFqn(), context);
if (machineEvents.isEmpty()) {
machineEvents = polymorphicEventsFromTransitions(machineTransitions, machineTypes.eventTypeFqn());
}
List<String> machineEvents = inferPolymorphicCandidates(
expanded.getConstraint(),
machineTypes.eventTypeFqn(),
machineTransitions,
context);
if (machineEvents.isEmpty()) {
return expanded;
}
return expanded.toBuilder().polymorphicEvents(machineEvents).ambiguous(true).build();
return expanded.toBuilder()
.polymorphicEvents(machineEvents)
.ambiguous(machineEvents.size() > 1)
.build();
}
private static List<String> inferPolymorphicCandidates(
String constraint,
String eventTypeFqn,
List<Transition> machineTransitions,
CodebaseContext context) {
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
if (!transitionEvents.isEmpty()) {
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
transitionEvents, constraint, eventTypeFqn, context);
return filtered.isEmpty() ? transitionEvents : filtered;
}
List<String> enumConstants = allPackageCanonicalEnumConstants(eventTypeFqn, context);
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
enumConstants, constraint, eventTypeFqn, context);
if (!filtered.isEmpty()) {
return filtered;
}
return List.of();
}
private static List<String> narrowPolymorphicCandidates(
List<String> current,
String constraint,
String eventTypeFqn,
List<Transition> machineTransitions,
CodebaseContext context) {
List<String> result = EnumMemberPredicateEvaluator.filterEnumConstants(
current, constraint, eventTypeFqn, context);
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
if (!looksOverBroad(result, transitionEvents, constraint)) {
return result;
}
if (!transitionEvents.isEmpty()) {
List<String> intersected = new ArrayList<>();
for (String event : result) {
if (transitionEvents.contains(event)) {
intersected.add(event);
}
}
if (!intersected.isEmpty()) {
return intersected;
}
List<String> transitionFiltered = EnumMemberPredicateEvaluator.filterEnumConstants(
transitionEvents, constraint, eventTypeFqn, context);
return transitionFiltered.isEmpty() ? transitionEvents : transitionFiltered;
}
return result;
}
private static boolean looksOverBroad(
List<String> current,
List<String> transitionEvents,
String constraint) {
if (current == null || current.isEmpty()) {
return false;
}
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(constraint)) {
return true;
}
if (current.size() <= 1) {
return false;
}
if (transitionEvents.isEmpty()) {
return current.size() > 1;
}
return current.size() > transitionEvents.size();
}
static List<String> polymorphicEventsFromTransitions(