B
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user