This commit is contained in:
2026-07-17 08:21:57 +02:00
parent 2c20b2c3fe
commit 32847d112b
9 changed files with 210 additions and 45 deletions

View File

@@ -4,6 +4,7 @@ import click.kamil.springstatemachineexporter.analysis.enricher.path.SymbolicPat
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstraintClauses;
import click.kamil.springstatemachineexporter.analysis.resolver.DynamicTriggerExpressions;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
@@ -43,10 +44,6 @@ public final class CallChainPolyNarrower {
private static final int MEDIUM = 2;
private static final int WEAK = 1;
private static final Pattern QUOTED_EQUALS_PARAM = Pattern.compile(
"\"([^\"]+)\"\\.equalsIgnoreCase\\((\\w+)\\)");
private static final Pattern PARAM_EQUALS_LITERAL = Pattern.compile(
"(\\w+)\\.equals(?:IgnoreCase)?\\(\"([^\"]+)\"\\)");
private static final Pattern ENUM_EQ = Pattern.compile(
"([A-Za-z_][\\w.]*)\\s*==\\s*([A-Za-z_][\\w.]*)");
private static final Pattern ROUTE_LITERAL = Pattern.compile(
@@ -177,13 +174,8 @@ public final class CallChainPolyNarrower {
if (constraint == null || constraint.isBlank()) {
return;
}
Matcher quoted = QUOTED_EQUALS_PARAM.matcher(constraint);
while (quoted.find()) {
String literal = quoted.group(1);
String param = quoted.group(2);
if (EventCarrierPolicy.isCarrierParamName(param)) {
for (String literal : ConstraintClauses.carrierLiterals(constraint)) {
addPathToken(literal, configuredConstants, votes, MEDIUM);
}
if (literal.contains(".")) {
addPathToken(literal, configuredConstants, votes, MEDIUM);
}
@@ -197,14 +189,6 @@ public final class CallChainPolyNarrower {
addEnumReference(enumEq.group(1), configuredConstants, votes, MEDIUM);
addEnumReference(enumEq.group(2), configuredConstants, votes, MEDIUM);
}
Matcher reverseEquals = PARAM_EQUALS_LITERAL.matcher(constraint);
while (reverseEquals.find()) {
String param = reverseEquals.group(1);
String literal = reverseEquals.group(2);
if (EventCarrierPolicy.isCarrierParamName(param)) {
addPathToken(literal, configuredConstants, votes, MEDIUM);
}
}
}
private static void collectFromSendEventLiterals(

View File

@@ -8,7 +8,6 @@ import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMar
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.service.CarrierConstraintSupport;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.analysis.service.EventCarrierPolicy;
import click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory;
@@ -72,7 +71,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
}
tp = MachineEnumCanonicalizer.expandBoundValueOfFromConstraints(
tp, effectiveMachineTypes, context, chain.getEntryPoint());
tp, effectiveMachineTypes, context, chain.getEntryPoint(), chain.getPathBindings());
tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
tp, effectiveMachineTypes, context, stateMachineTransitions, true);
tp = CallChainPolyNarrower.narrowForCallChain(
@@ -281,9 +280,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
if (constraint == null || machineName == null) {
return true;
}
String machineConstraint = CarrierConstraintSupport.stripNonMachineClauses(constraint);
return BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
machineConstraint, MachineDomainKeys.extractMachineDomainKey(machineName));
constraint, MachineDomainKeys.extractMachineDomainKey(machineName));
}
private String extractStateEnumType(String stateIdentifier) {

View File

@@ -51,11 +51,17 @@ public final class BooleanConstraintEvaluator {
+ Pattern.quote(literal) + "[\"']?\\s*\\)";
String regex3 = "(?i)[a-zA-Z0-9._]+\\s*==\\s*[\"']?" + Pattern.quote(literal) + "[\"']?";
String regex4 = "(?i)[a-zA-Z0-9._]+\\s*!=\\s*[\"']?" + Pattern.quote(literal) + "[\"']?";
String regexObjectsLitFirst = "(?i)(?:java\\.util\\.)?Objects\\.equals(?:IgnoreCase)?\\s*\\(\\s*[\"']?"
+ Pattern.quote(literal) + "[\"']?\\s*,\\s*[^)]+\\)";
String regexObjectsLitSecond = "(?i)(?:java\\.util\\.)?Objects\\.equals(?:IgnoreCase)?\\s*\\(\\s*[^,]+\\s*,\\s*[\"']?"
+ Pattern.quote(literal) + "[\"']?\\s*\\)";
expr = expr.replaceAll(regex1, replacement);
expr = expr.replaceAll(regex2, replacement);
expr = expr.replaceAll(regex3, replacement);
expr = expr.replaceAll(regex4, matchesMachine ? "false" : "true");
expr = expr.replaceAll(regexObjectsLitFirst, replacement);
expr = expr.replaceAll(regexObjectsLitSecond, replacement);
}
return evaluateBooleanExpression(expr);

View File

@@ -25,6 +25,19 @@ public final class ConstraintClauses {
+ "|"
+ "(?i)(?:\"[^\"]+\"|'[^']+'|[A-Za-z_][A-Za-z0-9_.]*)\\s*(?:==|!=)\\s*([A-Za-z_][A-Za-z0-9_]*)");
/**
* {@code Objects.equals(a, b)} / {@code Objects.equalsIgnoreCase(a, b)} with either side a
* quoted literal and the other a simple identifier (both argument orders).
*/
private static final Pattern OBJECTS_EQUALS = Pattern.compile(
"(?i)(?:java\\.util\\.)?Objects\\.equals(?:IgnoreCase)?\\s*\\(\\s*"
+ "(?:\"([^\"]+)\"|'([^']+)'|([A-Za-z_][A-Za-z0-9_]*))"
+ "\\s*,\\s*"
+ "(?:\"([^\"]+)\"|'([^']+)'|([A-Za-z_][A-Za-z0-9_]*))"
+ "\\s*\\)");
private static final Pattern QUOTED_LITERAL = Pattern.compile("\"([^\"]+)\"|'([^']+)'");
private ConstraintClauses() {
}
@@ -82,6 +95,17 @@ public final class ConstraintClauses {
return null;
}
String trimmed = clause.trim();
Matcher objects = OBJECTS_EQUALS.matcher(trimmed);
if (objects.find()) {
String leftIdent = objects.group(3);
String rightIdent = objects.group(6);
if (leftIdent != null && looksLikeSimpleIdent(leftIdent)) {
return leftIdent;
}
if (rightIdent != null && looksLikeSimpleIdent(rightIdent)) {
return rightIdent;
}
}
Matcher equals = EQUALS_CALL.matcher(trimmed);
if (equals.find()) {
String a = equals.group(1);
@@ -100,6 +124,53 @@ public final class ConstraintClauses {
return null;
}
/**
* First quoted string literal in a clause (equals / {@code ==} / {@code Objects.equals}).
*/
public static String quotedLiteral(String clause) {
if (clause == null || clause.isBlank()) {
return null;
}
String trimmed = clause.trim();
Matcher objects = OBJECTS_EQUALS.matcher(trimmed);
if (objects.find()) {
String left = firstNonNull(objects.group(1), objects.group(2));
if (left != null) {
return left;
}
String right = firstNonNull(objects.group(4), objects.group(5));
if (right != null) {
return right;
}
}
Matcher quoted = QUOTED_LITERAL.matcher(trimmed);
if (quoted.find()) {
return firstNonNull(quoted.group(1), quoted.group(2));
}
return null;
}
/**
* Quoted literals from AND-clauses whose primary identifier is an event/payload carrier.
*/
public static List<String> carrierLiterals(String constraint) {
List<String> literals = new ArrayList<>();
if (constraint == null || constraint.isBlank()) {
return literals;
}
for (String clause : splitAndClauses(constraint)) {
String ident = primaryIdent(clause);
if (ident == null || !EventCarrierPolicy.isCarrierParamName(ident)) {
continue;
}
String literal = quotedLiteral(clause);
if (literal != null && !literal.isBlank()) {
literals.add(literal);
}
}
return literals;
}
/**
* Union of primary identifiers across AND clauses (for binding overlap checks).
*/
@@ -121,6 +192,12 @@ public final class ConstraintClauses {
char first = value.charAt(0);
return Character.isJavaIdentifierStart(first)
&& value.chars().allMatch(Character::isJavaIdentifierPart)
&& (Character.isLowerCase(first) || EventCarrierPolicy.isCarrierParamName(value));
&& (Character.isLowerCase(first)
|| EventCarrierPolicy.isCarrierParamName(value)
|| EventCarrierPolicy.isMachineDiscriminatorName(value));
}
private static String firstNonNull(String a, String b) {
return a != null ? a : b;
}
}

View File

@@ -12,10 +12,9 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -1152,7 +1151,7 @@ public final class MachineEnumCanonicalizer {
TriggerPoint trigger,
StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context) {
return expandBoundValueOfFromConstraints(trigger, machineTypes, context, null);
return expandBoundValueOfFromConstraints(trigger, machineTypes, context, null, null);
}
public static TriggerPoint expandBoundValueOfFromConstraints(
@@ -1160,6 +1159,15 @@ public final class MachineEnumCanonicalizer {
StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context,
EntryPoint entryPoint) {
return expandBoundValueOfFromConstraints(trigger, machineTypes, context, entryPoint, null);
}
public static TriggerPoint expandBoundValueOfFromConstraints(
TriggerPoint trigger,
StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context,
EntryPoint entryPoint,
Map<String, String> pathBindings) {
if (trigger == null || trigger.getEvent() == null || machineTypes == null) {
return trigger;
}
@@ -1168,9 +1176,13 @@ public final class MachineEnumCanonicalizer {
}
String constraint = trigger.getConstraint();
List<String> boundEventLiterals = constraint != null && !constraint.isBlank()
? extractBoundEventLiteralsFromConstraint(constraint)
? ConstraintClauses.carrierLiterals(constraint)
: List.of();
// Dataflow-only: never invent event constants from REST URL path segments.
if (boundEventLiterals.size() != 1) {
boundEventLiterals = carrierLiteralsFromPathBindings(pathBindings);
}
// Dataflow-only: never invent event constants from REST URL path segments /
// unbound templates — only concrete expander bindings or constraint literals.
if (boundEventLiterals.size() != 1) {
return trigger;
}
@@ -1199,26 +1211,33 @@ public final class MachineEnumCanonicalizer {
.build();
}
private static List<String> extractBoundEventLiteralsFromConstraint(String constraint) {
List<String> literals = new ArrayList<>();
if (constraint == null || constraint.isBlank()) {
return literals;
private static List<String> carrierLiteralsFromPathBindings(Map<String, String> pathBindings) {
if (pathBindings == null || pathBindings.isEmpty()) {
return List.of();
}
Pattern quoted = Pattern.compile("\"([^\"]+)\"|'([^']+)'");
for (String clause : ConstraintClauses.splitAndClauses(constraint)) {
String ident = ConstraintClauses.primaryIdent(clause);
if (ident == null || !EventCarrierPolicy.isCarrierParamName(ident)) {
LinkedHashSet<String> literals = new LinkedHashSet<>();
for (Map.Entry<String, String> entry : pathBindings.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (key == null || value == null || value.isBlank()) {
continue;
}
Matcher matcher = quoted.matcher(clause);
if (matcher.find()) {
String literal = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
if (literal != null && !literal.isBlank()) {
literals.add(literal);
if (!EventCarrierPolicy.isCarrierParamName(key)) {
continue;
}
// Skip unbound / self-referential expander placeholders.
if (value.equals(key) || value.contains("{") || value.contains("(")) {
continue;
}
String cleaned = value;
if (cleaned.startsWith("\"") && cleaned.endsWith("\"") && cleaned.length() >= 2) {
cleaned = cleaned.substring(1, cleaned.length() - 1);
}
if (!cleaned.isBlank()) {
literals.add(cleaned);
}
}
}
return literals;
return new ArrayList<>(literals);
}
/**
@@ -1241,7 +1260,7 @@ public final class MachineEnumCanonicalizer {
}
String constraint = trigger.getConstraint();
List<String> bound = constraint != null && !constraint.isBlank()
? extractBoundEventLiteralsFromConstraint(constraint)
? ConstraintClauses.carrierLiterals(constraint)
: List.of();
return bound.size() != 1;
}

View File

@@ -70,6 +70,19 @@ class BooleanConstraintEvaluatorTest {
"OrderStateMachineConfiguration")).isTrue();
}
@Test
void shouldEvaluateObjectsEqualsAgainstMachineDomain() {
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
"Objects.equals(\"ORDER\", machineType)",
"OrderStateMachineConfiguration")).isTrue();
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
"Objects.equals(machineType, \"PAYMENT\")",
"OrderStateMachineConfiguration")).isFalse();
assertThat(BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
"Objects.equals(\"ORDER\", machineType) && Objects.equals(\"PAY\", message)",
"OrderStateMachineConfiguration")).isTrue();
}
@Test
void evaluateBooleanExpressionShouldHandleLogicalOperators() {
assertThat(BooleanConstraintEvaluator.evaluateBooleanExpression("true && false")).isFalse();

View File

@@ -46,4 +46,23 @@ class ConstraintClausesTest {
"\"ORDER\".equalsIgnoreCase(machineType) && message == \"PAY\""))
.containsExactly("machineType", "message");
}
@Test
void primaryIdentExtractsObjectsEqualsIdent() {
assertThat(ConstraintClauses.primaryIdent("Objects.equals(\"PAY\", message)"))
.isEqualTo("message");
assertThat(ConstraintClauses.primaryIdent("Objects.equals(machineType, \"ORDER\")"))
.isEqualTo("machineType");
assertThat(ConstraintClauses.primaryIdent("java.util.Objects.equalsIgnoreCase(\"ORDER\", type)"))
.isEqualTo("type");
}
@Test
void carrierLiteralsIncludeObjectsEqualsForms() {
assertThat(ConstraintClauses.carrierLiterals(
"Objects.equals(\"PAY\", message) && Objects.equals(machineType, \"ORDER\")"))
.containsExactly("PAY");
assertThat(ConstraintClauses.carrierLiterals("message == \"SHIP\""))
.containsExactly("SHIP");
}
}

View File

@@ -76,6 +76,46 @@ class MachineEnumCanonicalizerBoundValueOfTest {
assertThat(expanded.isAmbiguous()).isTrue();
}
@Test
void shouldExpandBoundValueOfFromPathBindingsAlone() {
TriggerPoint trigger = TriggerPoint.builder()
.event("OrderEvent.valueOf(message)")
.external(true)
.build();
MachineTypes machineTypes = new MachineTypes(
"com.example.order.OrderState",
"com.example.order.OrderEvent");
TriggerPoint expanded = MachineEnumCanonicalizer.expandBoundValueOfFromConstraints(
trigger,
machineTypes,
null,
null,
java.util.Map.of("message", "PAY"));
assertThat(expanded.getPolymorphicEvents())
.containsExactly("com.example.order.OrderEvent.PAY");
assertThat(expanded.isExternal()).isFalse();
}
@Test
void shouldExpandBoundValueOfFromObjectsEqualsConstraint() {
TriggerPoint trigger = TriggerPoint.builder()
.event("OrderEvent.valueOf(message)")
.constraint("Objects.equals(\"PAY\", message)")
.external(true)
.build();
MachineTypes machineTypes = new MachineTypes(
"com.example.order.OrderState",
"com.example.order.OrderEvent");
TriggerPoint expanded = MachineEnumCanonicalizer.expandBoundValueOfFromConstraints(
trigger, machineTypes, null);
assertThat(expanded.getPolymorphicEvents())
.containsExactly("com.example.order.OrderEvent.PAY");
}
private static Transition transition(String raw, String full) {
Transition t = new Transition();
t.setEvent(Event.of(raw, full));

View File

@@ -16,6 +16,15 @@ class CarrierConstraintSupportTest {
assertThat(stripped).isEqualToIgnoringWhitespace("true && true");
}
@Test
void stripNonMachineClausesRemovesObjectsEqualsCarrier() {
String stripped = CarrierConstraintSupport.stripNonMachineClauses(
"Objects.equals(\"PAY\", message) && Objects.equals(\"ORDER\", machineType)");
assertThat(stripped).contains("machineType");
assertThat(stripped).doesNotContain("message");
assertThat(stripped).contains("true");
}
@Test
void stripNonMachineClausesKeepsMachineDomainClauses() {
String stripped = CarrierConstraintSupport.stripNonMachineClauses(