B
This commit is contained in:
@@ -70,10 +70,12 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
tp = MachineEnumCanonicalizer.expandBoundValueOfFromConstraints(
|
tp = MachineEnumCanonicalizer.prepareCallChainTrigger(
|
||||||
tp, effectiveMachineTypes, context, chain.getEntryPoint(), chain.getPathBindings());
|
tp,
|
||||||
tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
effectiveMachineTypes,
|
||||||
tp, effectiveMachineTypes, context, stateMachineTransitions, true);
|
context,
|
||||||
|
stateMachineTransitions,
|
||||||
|
chain.getPathBindings());
|
||||||
tp = CallChainPolyNarrower.narrowForCallChain(
|
tp = CallChainPolyNarrower.narrowForCallChain(
|
||||||
tp, chain, effectiveMachineTypes, result.getEventTypeFqn(), stateMachineTransitions, context);
|
tp, chain, effectiveMachineTypes, result.getEventTypeFqn(), stateMachineTransitions, context);
|
||||||
tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, effectiveMachineTypes, context);
|
tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, effectiveMachineTypes, context);
|
||||||
|
|||||||
@@ -45,8 +45,12 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
|||||||
if (chain.getTriggerPoint() == null) {
|
if (chain.getTriggerPoint() == null) {
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
TriggerPoint expanded = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
TriggerPoint expanded = MachineEnumCanonicalizer.prepareCallChainTrigger(
|
||||||
chain.getTriggerPoint(), machineTypes, context, result.getTransitions(), true);
|
chain.getTriggerPoint(),
|
||||||
|
machineTypes,
|
||||||
|
context,
|
||||||
|
result.getTransitions(),
|
||||||
|
chain.getPathBindings());
|
||||||
TriggerPoint canonical = expanded.getEventTypeFqn() == null && expanded.getStateTypeFqn() == null
|
TriggerPoint canonical = expanded.getEventTypeFqn() == null && expanded.getStateTypeFqn() == null
|
||||||
? MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(
|
? MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(
|
||||||
expanded, machineTypes, context)
|
expanded, machineTypes, context)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.resolver;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.service.CarrierConstraintSupport;
|
import click.kamil.springstatemachineexporter.analysis.service.CarrierConstraintSupport;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -30,41 +31,93 @@ public final class BooleanConstraintEvaluator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop event/payload carrier clauses so they never enter the domain regex path.
|
// Drop event/payload carrier clauses so they never enter the domain rewrite path.
|
||||||
constraint = CarrierConstraintSupport.stripNonMachineClauses(constraint);
|
constraint = CarrierConstraintSupport.stripNonMachineClauses(constraint);
|
||||||
if (constraint == null || constraint.isBlank()) {
|
if (constraint == null || constraint.isBlank()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String cleanMachine = machineDomainKey.toUpperCase();
|
String cleanMachine = machineDomainKey.toUpperCase(Locale.ROOT);
|
||||||
String expr = constraint;
|
List<String> rewritten = new ArrayList<>();
|
||||||
Set<String> literals = extractStringLiterals(constraint);
|
for (String clause : ConstraintClauses.splitAndClauses(constraint)) {
|
||||||
|
rewritten.add(rewriteDomainEqualityClause(clause, cleanMachine));
|
||||||
for (String literal : literals) {
|
|
||||||
boolean matchesMachine = literal.equalsIgnoreCase(cleanMachine)
|
|
||||||
|| cleanMachine.equalsIgnoreCase(literal)
|
|
||||||
|| (cleanMachine.length() > literal.length() && cleanMachine.startsWith(literal.toUpperCase()));
|
|
||||||
String replacement = matchesMachine ? "true" : "false";
|
|
||||||
|
|
||||||
String regex1 = "(?i)[\"']?" + Pattern.quote(literal) + "[\"']?\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\([^)]*\\)";
|
|
||||||
String regex2 = "(?i)[a-zA-Z0-9._]+\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\(\\s*[\"']?"
|
|
||||||
+ 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);
|
|
||||||
}
|
}
|
||||||
|
if (rewritten.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return evaluateBooleanExpression(String.join(" && ", rewritten));
|
||||||
|
}
|
||||||
|
|
||||||
return evaluateBooleanExpression(expr);
|
/**
|
||||||
|
* Simple equality clauses collapse via {@link ConstraintClauses}; complex clauses ({@code !},
|
||||||
|
* {@code ||}, nested parens) keep a residual literal rewrite so negation/OR stay correct.
|
||||||
|
*/
|
||||||
|
private static String rewriteDomainEqualityClause(String clause, String cleanMachine) {
|
||||||
|
if (clause == null || clause.isBlank()) {
|
||||||
|
return clause;
|
||||||
|
}
|
||||||
|
String trimmed = clause.trim();
|
||||||
|
if ("true".equalsIgnoreCase(trimmed) || "false".equalsIgnoreCase(trimmed)) {
|
||||||
|
return trimmed.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
boolean complex = trimmed.indexOf('!') >= 0
|
||||||
|
|| trimmed.contains("||")
|
||||||
|
|| (trimmed.indexOf('(') >= 0 && !isObjectsEqualsClause(trimmed));
|
||||||
|
if (!complex) {
|
||||||
|
String literal = ConstraintClauses.quotedLiteral(trimmed);
|
||||||
|
if (literal != null && ConstraintClauses.primaryIdent(trimmed) != null) {
|
||||||
|
boolean matchesMachine = domainLiteralMatches(literal, cleanMachine);
|
||||||
|
boolean inequality = trimmed.contains("!=");
|
||||||
|
if (inequality) {
|
||||||
|
return matchesMachine ? "false" : "true";
|
||||||
|
}
|
||||||
|
return matchesMachine ? "true" : "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rewriteDomainLiteralsWithRegex(trimmed, cleanMachine);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isObjectsEqualsClause(String clause) {
|
||||||
|
return clause.matches("(?is)\\s*(?:java\\.util\\.)?Objects\\.equals(?:IgnoreCase)?\\s*\\(.*\\)\\s*");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean domainLiteralMatches(String literal, String cleanMachine) {
|
||||||
|
return literal.equalsIgnoreCase(cleanMachine)
|
||||||
|
|| cleanMachine.equalsIgnoreCase(literal)
|
||||||
|
|| (cleanMachine.length() > literal.length()
|
||||||
|
&& cleanMachine.startsWith(literal.toUpperCase(Locale.ROOT)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String rewriteDomainLiteralsWithRegex(String expr, String cleanMachine) {
|
||||||
|
Set<String> literals = extractStringLiterals(expr);
|
||||||
|
String result = expr;
|
||||||
|
for (String literal : literals) {
|
||||||
|
boolean matchesMachine = domainLiteralMatches(literal, cleanMachine);
|
||||||
|
String replacement = matchesMachine ? "true" : "false";
|
||||||
|
String quoted = Pattern.quote(literal);
|
||||||
|
result = result.replaceAll(
|
||||||
|
"(?i)[\"']?" + quoted + "[\"']?\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\([^)]*\\)",
|
||||||
|
replacement);
|
||||||
|
result = result.replaceAll(
|
||||||
|
"(?i)[a-zA-Z0-9._]+\\s*\\.\\s*(equalsIgnoreCase|equals)\\s*\\(\\s*[\"']?"
|
||||||
|
+ quoted + "[\"']?\\s*\\)",
|
||||||
|
replacement);
|
||||||
|
result = result.replaceAll(
|
||||||
|
"(?i)[a-zA-Z0-9._]+\\s*==\\s*[\"']?" + quoted + "[\"']?",
|
||||||
|
replacement);
|
||||||
|
result = result.replaceAll(
|
||||||
|
"(?i)[a-zA-Z0-9._]+\\s*!=\\s*[\"']?" + quoted + "[\"']?",
|
||||||
|
matchesMachine ? "false" : "true");
|
||||||
|
result = result.replaceAll(
|
||||||
|
"(?i)(?:java\\.util\\.)?Objects\\.equals(?:IgnoreCase)?\\s*\\(\\s*[\"']?"
|
||||||
|
+ quoted + "[\"']?\\s*,\\s*[^)]+\\)",
|
||||||
|
replacement);
|
||||||
|
result = result.replaceAll(
|
||||||
|
"(?i)(?:java\\.util\\.)?Objects\\.equals(?:IgnoreCase)?\\s*\\(\\s*[^,]+\\s*,\\s*[\"']?"
|
||||||
|
+ quoted + "[\"']?\\s*\\)",
|
||||||
|
replacement);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean evaluateBooleanExpression(String expression) {
|
public static boolean evaluateBooleanExpression(String expression) {
|
||||||
@@ -251,7 +304,7 @@ public final class BooleanConstraintEvaluator {
|
|||||||
|
|
||||||
private static String replaceObjectsEqualsForVariable(
|
private static String replaceObjectsEqualsForVariable(
|
||||||
String expr, String varName, String literalValue, String replacement) {
|
String expr, String varName, String literalValue, String replacement) {
|
||||||
Pattern head = Pattern.compile("(?is)(?:java\\.util\\.)?Objects\\.equals\\s*\\(");
|
Pattern head = Pattern.compile("(?is)(?:java\\.util\\.)?Objects\\.equals(?:IgnoreCase)?\\s*\\(");
|
||||||
Matcher matcher = head.matcher(expr);
|
Matcher matcher = head.matcher(expr);
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
int cursor = 0;
|
int cursor = 0;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import click.kamil.springstatemachineexporter.analysis.service.EventCarrierPolic
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -171,6 +172,56 @@ public final class ConstraintClauses {
|
|||||||
return literals;
|
return literals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concrete expander/path-binding values for carrier keys (skips self-keys, placeholders,
|
||||||
|
* and method-call shaped values). Used by bound valueOf, unbound-widen gates, and poly-narrow.
|
||||||
|
*/
|
||||||
|
public static List<String> carrierLiteralsFromPathBindings(Map<String, String> pathBindings) {
|
||||||
|
if (pathBindings == null || pathBindings.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (!EventCarrierPolicy.isCarrierParamName(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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 new ArrayList<>(literals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union of carrier literals from constraint text and path bindings (constraint first).
|
||||||
|
*/
|
||||||
|
public static List<String> boundCarrierLiterals(String constraint, Map<String, String> pathBindings) {
|
||||||
|
List<String> fromConstraint = carrierLiterals(constraint);
|
||||||
|
if (fromConstraint.size() == 1) {
|
||||||
|
return fromConstraint;
|
||||||
|
}
|
||||||
|
List<String> fromBindings = carrierLiteralsFromPathBindings(pathBindings);
|
||||||
|
if (fromBindings.size() == 1) {
|
||||||
|
return fromBindings;
|
||||||
|
}
|
||||||
|
if (!fromConstraint.isEmpty()) {
|
||||||
|
return fromConstraint;
|
||||||
|
}
|
||||||
|
return fromBindings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Union of primary identifiers across AND clauses (for binding overlap checks).
|
* Union of primary identifiers across AND clauses (for binding overlap checks).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
List<PredicateCall> calls = new ArrayList<>();
|
List<PredicateCall> calls = new ArrayList<>();
|
||||||
for (String part : splitTopLevelAndParts(constraint)) {
|
for (String part : ConstraintClauses.splitAndClauses(constraint)) {
|
||||||
Matcher matcher = PREDICATE_PART.matcher(part.trim());
|
Matcher matcher = PREDICATE_PART.matcher(part.trim());
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
boolean negated = part.trim().startsWith("!");
|
boolean negated = part.trim().startsWith("!");
|
||||||
@@ -558,29 +558,4 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
}
|
}
|
||||||
return canonicalConstantFqn.substring(canonicalConstantFqn.lastIndexOf('.') + 1);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.EventCarrierPolicy;
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import click.kamil.springstatemachineexporter.model.Event;
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
import click.kamil.springstatemachineexporter.model.State;
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
@@ -261,7 +259,7 @@ public final class MachineEnumCanonicalizer {
|
|||||||
TriggerPoint trigger,
|
TriggerPoint trigger,
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
CodebaseContext context) {
|
CodebaseContext context) {
|
||||||
return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, null, false);
|
return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, null, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TriggerPoint ensureCallChainPolymorphicEvents(
|
public static TriggerPoint ensureCallChainPolymorphicEvents(
|
||||||
@@ -269,7 +267,7 @@ public final class MachineEnumCanonicalizer {
|
|||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
CodebaseContext context,
|
CodebaseContext context,
|
||||||
List<Transition> machineTransitions) {
|
List<Transition> machineTransitions) {
|
||||||
return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, machineTransitions, false);
|
return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, machineTransitions, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TriggerPoint ensureCallChainPolymorphicEvents(
|
public static TriggerPoint ensureCallChainPolymorphicEvents(
|
||||||
@@ -278,6 +276,34 @@ public final class MachineEnumCanonicalizer {
|
|||||||
CodebaseContext context,
|
CodebaseContext context,
|
||||||
List<Transition> machineTransitions,
|
List<Transition> machineTransitions,
|
||||||
boolean skipCanonicalization) {
|
boolean skipCanonicalization) {
|
||||||
|
return ensureCallChainPolymorphicEvents(
|
||||||
|
trigger, machineTypes, context, machineTransitions, skipCanonicalization, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared call-chain trigger prep: bound {@code valueOf} from constraint/pathBindings, then
|
||||||
|
* polymorphic ensure/widen. Used by linker, canonicalization, and finalizer so all flows share
|
||||||
|
* the same pathBindings-aware channel.
|
||||||
|
*/
|
||||||
|
public static TriggerPoint prepareCallChainTrigger(
|
||||||
|
TriggerPoint trigger,
|
||||||
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
|
CodebaseContext context,
|
||||||
|
List<Transition> machineTransitions,
|
||||||
|
Map<String, String> pathBindings) {
|
||||||
|
TriggerPoint expanded = expandBoundValueOfFromConstraints(
|
||||||
|
trigger, machineTypes, context, pathBindings);
|
||||||
|
return ensureCallChainPolymorphicEvents(
|
||||||
|
expanded, machineTypes, context, machineTransitions, true, pathBindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TriggerPoint ensureCallChainPolymorphicEvents(
|
||||||
|
TriggerPoint trigger,
|
||||||
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
|
CodebaseContext context,
|
||||||
|
List<Transition> machineTransitions,
|
||||||
|
boolean skipCanonicalization,
|
||||||
|
Map<String, String> pathBindings) {
|
||||||
TriggerPoint expanded = skipCanonicalization
|
TriggerPoint expanded = skipCanonicalization
|
||||||
? expandSymbolicOnly(trigger, machineTypes, context, machineTransitions)
|
? expandSymbolicOnly(trigger, machineTypes, context, machineTransitions)
|
||||||
: canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
|
: canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
|
||||||
@@ -319,7 +345,7 @@ public final class MachineEnumCanonicalizer {
|
|||||||
}
|
}
|
||||||
if (expanded.getPolymorphicEvents() != null
|
if (expanded.getPolymorphicEvents() != null
|
||||||
&& !expanded.getPolymorphicEvents().isEmpty()
|
&& !expanded.getPolymorphicEvents().isEmpty()
|
||||||
&& !isUnboundValueOfWidenCandidate(expanded)) {
|
&& !isUnboundValueOfWidenCandidate(expanded, pathBindings)) {
|
||||||
List<String> narrowed = narrowPolymorphicCandidates(
|
List<String> narrowed = narrowPolymorphicCandidates(
|
||||||
expanded.getPolymorphicEvents(),
|
expanded.getPolymorphicEvents(),
|
||||||
expanded.getConstraint(),
|
expanded.getConstraint(),
|
||||||
@@ -351,8 +377,12 @@ public final class MachineEnumCanonicalizer {
|
|||||||
.ambiguous(machineEvents.size() > 1)
|
.ambiguous(machineEvents.size() > 1)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
// Predicate-driven inference failed closed — do not fall through to unbound valueOf widen.
|
||||||
|
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(expanded.getConstraint())) {
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isUnboundValueOfWidenCandidate(expanded)) {
|
if (isUnboundValueOfWidenCandidate(expanded, pathBindings)) {
|
||||||
List<String> widened = widenUnboundValueOfEvents(
|
List<String> widened = widenUnboundValueOfEvents(
|
||||||
machineTypes.eventTypeFqn(), machineTransitions, context);
|
machineTypes.eventTypeFqn(), machineTransitions, context);
|
||||||
if (!widened.isEmpty()) {
|
if (!widened.isEmpty()) {
|
||||||
@@ -1151,22 +1181,13 @@ public final class MachineEnumCanonicalizer {
|
|||||||
TriggerPoint trigger,
|
TriggerPoint trigger,
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
CodebaseContext context) {
|
CodebaseContext context) {
|
||||||
return expandBoundValueOfFromConstraints(trigger, machineTypes, context, null, null);
|
return expandBoundValueOfFromConstraints(trigger, machineTypes, context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TriggerPoint expandBoundValueOfFromConstraints(
|
public static TriggerPoint expandBoundValueOfFromConstraints(
|
||||||
TriggerPoint trigger,
|
TriggerPoint trigger,
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
CodebaseContext context,
|
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) {
|
Map<String, String> pathBindings) {
|
||||||
if (trigger == null || trigger.getEvent() == null || machineTypes == null) {
|
if (trigger == null || trigger.getEvent() == null || machineTypes == null) {
|
||||||
return trigger;
|
return trigger;
|
||||||
@@ -1174,13 +1195,8 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (!isDynamicTriggerExpression(trigger.getEvent()) || !trigger.getEvent().contains(".valueOf(")) {
|
if (!isDynamicTriggerExpression(trigger.getEvent()) || !trigger.getEvent().contains(".valueOf(")) {
|
||||||
return trigger;
|
return trigger;
|
||||||
}
|
}
|
||||||
String constraint = trigger.getConstraint();
|
List<String> boundEventLiterals = ConstraintClauses.boundCarrierLiterals(
|
||||||
List<String> boundEventLiterals = constraint != null && !constraint.isBlank()
|
trigger.getConstraint(), pathBindings);
|
||||||
? ConstraintClauses.carrierLiterals(constraint)
|
|
||||||
: List.of();
|
|
||||||
if (boundEventLiterals.size() != 1) {
|
|
||||||
boundEventLiterals = carrierLiteralsFromPathBindings(pathBindings);
|
|
||||||
}
|
|
||||||
// Dataflow-only: never invent event constants from REST URL path segments /
|
// Dataflow-only: never invent event constants from REST URL path segments /
|
||||||
// unbound templates — only concrete expander bindings or constraint literals.
|
// unbound templates — only concrete expander bindings or constraint literals.
|
||||||
if (boundEventLiterals.size() != 1) {
|
if (boundEventLiterals.size() != 1) {
|
||||||
@@ -1211,40 +1227,12 @@ public final class MachineEnumCanonicalizer {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> carrierLiteralsFromPathBindings(Map<String, String> pathBindings) {
|
|
||||||
if (pathBindings == null || pathBindings.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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 new ArrayList<>(literals);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when the trigger is {@code Enum.valueOf(...)} without a single bound carrier literal
|
* True when the trigger is {@code Enum.valueOf(...)} without a single bound carrier literal
|
||||||
* and without concrete polymorphic events — eligible for enum-wide / transition-wide widen.
|
* and without concrete polymorphic events — eligible for enum-wide / transition-wide widen.
|
||||||
*/
|
*/
|
||||||
private static boolean isUnboundValueOfWidenCandidate(TriggerPoint trigger) {
|
private static boolean isUnboundValueOfWidenCandidate(
|
||||||
|
TriggerPoint trigger, Map<String, String> pathBindings) {
|
||||||
if (trigger == null || trigger.getEvent() == null) {
|
if (trigger == null || trigger.getEvent() == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1258,10 +1246,7 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (hasConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
|
if (hasConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String constraint = trigger.getConstraint();
|
List<String> bound = ConstraintClauses.boundCarrierLiterals(trigger.getConstraint(), pathBindings);
|
||||||
List<String> bound = constraint != null && !constraint.isBlank()
|
|
||||||
? ConstraintClauses.carrierLiterals(constraint)
|
|
||||||
: List.of();
|
|
||||||
return bound.size() != 1;
|
return bound.size() != 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanon
|
|||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator;
|
import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -100,7 +101,8 @@ public final class AnalysisResultFinalizer {
|
|||||||
if (result.getMetadata() != null) {
|
if (result.getMetadata() != null) {
|
||||||
CodebaseMetadata metadata = result.getMetadata();
|
CodebaseMetadata metadata = result.getMetadata();
|
||||||
List<TriggerPoint> triggers = canonicalizeTriggers(metadata.getTriggers(), context, machineTypes);
|
List<TriggerPoint> triggers = canonicalizeTriggers(metadata.getTriggers(), context, machineTypes);
|
||||||
List<CallChain> callChains = canonicalizeCallChains(metadata.getCallChains(), context, machineTypes);
|
List<CallChain> callChains = canonicalizeCallChains(
|
||||||
|
metadata.getCallChains(), context, machineTypes, result.getTransitions());
|
||||||
result.setMetadata(CodebaseMetadata.builder()
|
result.setMetadata(CodebaseMetadata.builder()
|
||||||
.triggers(triggers)
|
.triggers(triggers)
|
||||||
.entryPoints(metadata.getEntryPoints())
|
.entryPoints(metadata.getEntryPoints())
|
||||||
@@ -126,7 +128,8 @@ public final class AnalysisResultFinalizer {
|
|||||||
private static List<CallChain> canonicalizeCallChains(
|
private static List<CallChain> canonicalizeCallChains(
|
||||||
List<CallChain> callChains,
|
List<CallChain> callChains,
|
||||||
CodebaseContext context,
|
CodebaseContext context,
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
|
List<Transition> transitions) {
|
||||||
if (callChains == null) {
|
if (callChains == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -134,8 +137,16 @@ public final class AnalysisResultFinalizer {
|
|||||||
.map(chain -> {
|
.map(chain -> {
|
||||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
TriggerPoint trigger = chain.getTriggerPoint() == null
|
||||||
? null
|
? null
|
||||||
: MachineEnumCanonicalizer.canonicalizeAndExpandTriggerPoint(
|
: MachineEnumCanonicalizer.prepareCallChainTrigger(
|
||||||
chain.getTriggerPoint(), machineTypes, context);
|
chain.getTriggerPoint(),
|
||||||
|
machineTypes,
|
||||||
|
context,
|
||||||
|
transitions,
|
||||||
|
chain.getPathBindings());
|
||||||
|
if (trigger != null) {
|
||||||
|
trigger = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(
|
||||||
|
trigger, machineTypes, context);
|
||||||
|
}
|
||||||
List<MatchedTransition> matched = canonicalizeMatchedTransitions(
|
List<MatchedTransition> matched = canonicalizeMatchedTransitions(
|
||||||
chain.getMatchedTransitions(), machineTypes, context);
|
chain.getMatchedTransitions(), machineTypes, context);
|
||||||
return chain.toBuilder()
|
return chain.toBuilder()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstraintClauses;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
@@ -2008,7 +2009,7 @@ public final class EntryPointBindingExpander {
|
|||||||
if (constraint == null || constraint.isBlank() || context == null) {
|
if (constraint == null || constraint.isBlank() || context == null) {
|
||||||
return constraint;
|
return constraint;
|
||||||
}
|
}
|
||||||
List<String> parts = splitTopLevelAnd(constraint);
|
List<String> parts = ConstraintClauses.splitAndClauses(constraint);
|
||||||
if (parts.isEmpty()) {
|
if (parts.isEmpty()) {
|
||||||
return constraint;
|
return constraint;
|
||||||
}
|
}
|
||||||
@@ -2149,26 +2150,6 @@ public final class EntryPointBindingExpander {
|
|||||||
return findStaticImportHelper(helperName, compilationUnit, context);
|
return findStaticImportHelper(helperName, compilationUnit, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> splitTopLevelAnd(String constraint) {
|
|
||||||
List<String> parts = new ArrayList<>();
|
|
||||||
int depth = 0;
|
|
||||||
int start = 0;
|
|
||||||
for (int i = 0; i < constraint.length() - 1; i++) {
|
|
||||||
char c = constraint.charAt(i);
|
|
||||||
if (c == '(') {
|
|
||||||
depth++;
|
|
||||||
} else if (c == ')') {
|
|
||||||
depth--;
|
|
||||||
} else if (depth == 0 && c == '&' && constraint.charAt(i + 1) == '&') {
|
|
||||||
parts.add(constraint.substring(start, i));
|
|
||||||
start = i + 2;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parts.add(constraint.substring(start));
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String methodFqnFromNode(ASTNode node, CodebaseContext context) {
|
private static String methodFqnFromNode(ASTNode node, CodebaseContext context) {
|
||||||
MethodDeclaration method = AstUtils.findEnclosingMethod(node);
|
MethodDeclaration method = AstUtils.findEnclosingMethod(node);
|
||||||
TypeDeclaration type = AstUtils.findEnclosingType(node);
|
TypeDeclaration type = AstUtils.findEnclosingType(node);
|
||||||
|
|||||||
@@ -65,4 +65,26 @@ class ConstraintClausesTest {
|
|||||||
assertThat(ConstraintClauses.carrierLiterals("message == \"SHIP\""))
|
assertThat(ConstraintClauses.carrierLiterals("message == \"SHIP\""))
|
||||||
.containsExactly("SHIP");
|
.containsExactly("SHIP");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void carrierLiteralsFromPathBindingsSkipsPlaceholders() {
|
||||||
|
assertThat(ConstraintClauses.carrierLiteralsFromPathBindings(
|
||||||
|
java.util.Map.of("message", "PAY", "id", "42")))
|
||||||
|
.containsExactly("PAY");
|
||||||
|
assertThat(ConstraintClauses.carrierLiteralsFromPathBindings(
|
||||||
|
java.util.Map.of("message", "message", "event", "{event}")))
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void boundCarrierLiteralsPrefersSingleConstraintLiteral() {
|
||||||
|
assertThat(ConstraintClauses.boundCarrierLiterals(
|
||||||
|
"message == \"PAY\"",
|
||||||
|
java.util.Map.of("message", "SHIP")))
|
||||||
|
.containsExactly("PAY");
|
||||||
|
assertThat(ConstraintClauses.boundCarrierLiterals(
|
||||||
|
null,
|
||||||
|
java.util.Map.of("message", "SHIP")))
|
||||||
|
.containsExactly("SHIP");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ class MachineEnumCanonicalizerBoundValueOfTest {
|
|||||||
trigger,
|
trigger,
|
||||||
machineTypes,
|
machineTypes,
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
java.util.Map.of("message", "PAY"));
|
java.util.Map.of("message", "PAY"));
|
||||||
|
|
||||||
assertThat(expanded.getPolymorphicEvents())
|
assertThat(expanded.getPolymorphicEvents())
|
||||||
@@ -98,6 +97,31 @@ class MachineEnumCanonicalizerBoundValueOfTest {
|
|||||||
assertThat(expanded.isExternal()).isFalse();
|
assertThat(expanded.isExternal()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void prepareCallChainTriggerBindsFromPathBindingsAndBlocksWiden() {
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.event("OrderEvent.valueOf(message)")
|
||||||
|
.external(true)
|
||||||
|
.build();
|
||||||
|
MachineTypes machineTypes = new MachineTypes(
|
||||||
|
"com.example.order.OrderState",
|
||||||
|
"com.example.order.OrderEvent");
|
||||||
|
List<Transition> transitions = List.of(
|
||||||
|
transition("PAY", "com.example.order.OrderEvent.PAY"),
|
||||||
|
transition("SHIP", "com.example.order.OrderEvent.SHIP"));
|
||||||
|
|
||||||
|
TriggerPoint prepared = MachineEnumCanonicalizer.prepareCallChainTrigger(
|
||||||
|
trigger,
|
||||||
|
machineTypes,
|
||||||
|
null,
|
||||||
|
transitions,
|
||||||
|
java.util.Map.of("message", "PAY"));
|
||||||
|
|
||||||
|
assertThat(prepared.getPolymorphicEvents())
|
||||||
|
.containsExactly("com.example.order.OrderEvent.PAY");
|
||||||
|
assertThat(prepared.isAmbiguous()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExpandBoundValueOfFromObjectsEqualsConstraint() {
|
void shouldExpandBoundValueOfFromObjectsEqualsConstraint() {
|
||||||
TriggerPoint trigger = TriggerPoint.builder()
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
|||||||
Reference in New Issue
Block a user