diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java index a2b0eb9..1f930c2 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java @@ -70,10 +70,12 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { continue; } - tp = MachineEnumCanonicalizer.expandBoundValueOfFromConstraints( - tp, effectiveMachineTypes, context, chain.getEntryPoint(), chain.getPathBindings()); - tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents( - tp, effectiveMachineTypes, context, stateMachineTransitions, true); + tp = MachineEnumCanonicalizer.prepareCallChainTrigger( + tp, + effectiveMachineTypes, + context, + stateMachineTransitions, + chain.getPathBindings()); tp = CallChainPolyNarrower.narrowForCallChain( tp, chain, effectiveMachineTypes, result.getEventTypeFqn(), stateMachineTransitions, context); tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, effectiveMachineTypes, context); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerCanonicalizationEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerCanonicalizationEnricher.java index fb0f194..ab58e26 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerCanonicalizationEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TriggerCanonicalizationEnricher.java @@ -45,8 +45,12 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher { if (chain.getTriggerPoint() == null) { return chain; } - TriggerPoint expanded = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents( - chain.getTriggerPoint(), machineTypes, context, result.getTransitions(), true); + TriggerPoint expanded = MachineEnumCanonicalizer.prepareCallChainTrigger( + chain.getTriggerPoint(), + machineTypes, + context, + result.getTransitions(), + chain.getPathBindings()); TriggerPoint canonical = expanded.getEventTypeFqn() == null && expanded.getStateTypeFqn() == null ? MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking( expanded, machineTypes, context) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BooleanConstraintEvaluator.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BooleanConstraintEvaluator.java index ac99952..42397dd 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BooleanConstraintEvaluator.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/BooleanConstraintEvaluator.java @@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.resolver; import click.kamil.springstatemachineexporter.analysis.service.CarrierConstraintSupport; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -30,41 +31,93 @@ public final class BooleanConstraintEvaluator { 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); if (constraint == null || constraint.isBlank()) { return true; } - String cleanMachine = machineDomainKey.toUpperCase(); - String expr = constraint; - Set literals = extractStringLiterals(constraint); - - 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); + String cleanMachine = machineDomainKey.toUpperCase(Locale.ROOT); + List rewritten = new ArrayList<>(); + for (String clause : ConstraintClauses.splitAndClauses(constraint)) { + rewritten.add(rewriteDomainEqualityClause(clause, cleanMachine)); } + 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 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) { @@ -251,7 +304,7 @@ public final class BooleanConstraintEvaluator { private static String replaceObjectsEqualsForVariable( 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); StringBuilder result = new StringBuilder(); int cursor = 0; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstraintClauses.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstraintClauses.java index 5db558f..858f723 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstraintClauses.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstraintClauses.java @@ -5,6 +5,7 @@ import click.kamil.springstatemachineexporter.analysis.service.EventCarrierPolic import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -171,6 +172,56 @@ public final class ConstraintClauses { 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 carrierLiteralsFromPathBindings(Map pathBindings) { + if (pathBindings == null || pathBindings.isEmpty()) { + return List.of(); + } + LinkedHashSet literals = new LinkedHashSet<>(); + for (Map.Entry 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 boundCarrierLiterals(String constraint, Map pathBindings) { + List fromConstraint = carrierLiterals(constraint); + if (fromConstraint.size() == 1) { + return fromConstraint; + } + List 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). */ diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/EnumMemberPredicateEvaluator.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/EnumMemberPredicateEvaluator.java index 5afd0e5..a5495a8 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/EnumMemberPredicateEvaluator.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/EnumMemberPredicateEvaluator.java @@ -60,7 +60,7 @@ public final class EnumMemberPredicateEvaluator { return List.of(); } List calls = new ArrayList<>(); - for (String part : splitTopLevelAndParts(constraint)) { + for (String part : ConstraintClauses.splitAndClauses(constraint)) { Matcher matcher = PREDICATE_PART.matcher(part.trim()); if (matcher.matches()) { boolean negated = part.trim().startsWith("!"); @@ -558,29 +558,4 @@ public final class EnumMemberPredicateEvaluator { } return canonicalConstantFqn.substring(canonicalConstantFqn.lastIndexOf('.') + 1); } - - private static List splitTopLevelAndParts(String constraint) { - List 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; - } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java index 2705a87..b12dc45 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java @@ -1,8 +1,6 @@ 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.service.EventCarrierPolicy; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.model.Event; import click.kamil.springstatemachineexporter.model.State; @@ -261,7 +259,7 @@ public final class MachineEnumCanonicalizer { TriggerPoint trigger, StateMachineTypeResolver.MachineTypes machineTypes, CodebaseContext context) { - return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, null, false); + return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, null, false, null); } public static TriggerPoint ensureCallChainPolymorphicEvents( @@ -269,7 +267,7 @@ public final class MachineEnumCanonicalizer { StateMachineTypeResolver.MachineTypes machineTypes, CodebaseContext context, List machineTransitions) { - return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, machineTransitions, false); + return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, machineTransitions, false, null); } public static TriggerPoint ensureCallChainPolymorphicEvents( @@ -278,6 +276,34 @@ public final class MachineEnumCanonicalizer { CodebaseContext context, List machineTransitions, 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 machineTransitions, + Map 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 machineTransitions, + boolean skipCanonicalization, + Map pathBindings) { TriggerPoint expanded = skipCanonicalization ? expandSymbolicOnly(trigger, machineTypes, context, machineTransitions) : canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context); @@ -319,7 +345,7 @@ public final class MachineEnumCanonicalizer { } if (expanded.getPolymorphicEvents() != null && !expanded.getPolymorphicEvents().isEmpty() - && !isUnboundValueOfWidenCandidate(expanded)) { + && !isUnboundValueOfWidenCandidate(expanded, pathBindings)) { List narrowed = narrowPolymorphicCandidates( expanded.getPolymorphicEvents(), expanded.getConstraint(), @@ -351,8 +377,12 @@ public final class MachineEnumCanonicalizer { .ambiguous(machineEvents.size() > 1) .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 widened = widenUnboundValueOfEvents( machineTypes.eventTypeFqn(), machineTransitions, context); if (!widened.isEmpty()) { @@ -1151,22 +1181,13 @@ public final class MachineEnumCanonicalizer { TriggerPoint trigger, StateMachineTypeResolver.MachineTypes machineTypes, CodebaseContext context) { - return expandBoundValueOfFromConstraints(trigger, machineTypes, context, null, null); + return expandBoundValueOfFromConstraints(trigger, machineTypes, context, null); } public static TriggerPoint expandBoundValueOfFromConstraints( TriggerPoint trigger, 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 pathBindings) { if (trigger == null || trigger.getEvent() == null || machineTypes == null) { return trigger; @@ -1174,13 +1195,8 @@ public final class MachineEnumCanonicalizer { if (!isDynamicTriggerExpression(trigger.getEvent()) || !trigger.getEvent().contains(".valueOf(")) { return trigger; } - String constraint = trigger.getConstraint(); - List boundEventLiterals = constraint != null && !constraint.isBlank() - ? ConstraintClauses.carrierLiterals(constraint) - : List.of(); - if (boundEventLiterals.size() != 1) { - boundEventLiterals = carrierLiteralsFromPathBindings(pathBindings); - } + List boundEventLiterals = ConstraintClauses.boundCarrierLiterals( + trigger.getConstraint(), 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) { @@ -1211,40 +1227,12 @@ public final class MachineEnumCanonicalizer { .build(); } - private static List carrierLiteralsFromPathBindings(Map pathBindings) { - if (pathBindings == null || pathBindings.isEmpty()) { - return List.of(); - } - LinkedHashSet literals = new LinkedHashSet<>(); - for (Map.Entry 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 * 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 pathBindings) { if (trigger == null || trigger.getEvent() == null) { return false; } @@ -1258,10 +1246,7 @@ public final class MachineEnumCanonicalizer { if (hasConcretePolymorphicEvents(trigger.getPolymorphicEvents())) { return false; } - String constraint = trigger.getConstraint(); - List bound = constraint != null && !constraint.isBlank() - ? ConstraintClauses.carrierLiterals(constraint) - : List.of(); + List bound = ConstraintClauses.boundCarrierLiterals(trigger.getConstraint(), pathBindings); return bound.size() != 1; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AnalysisResultFinalizer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AnalysisResultFinalizer.java index 9b542e1..b67fd6b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AnalysisResultFinalizer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AnalysisResultFinalizer.java @@ -9,6 +9,7 @@ import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanon import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver; import click.kamil.springstatemachineexporter.analysis.validation.AnalysisCanonicalFormValidator; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import click.kamil.springstatemachineexporter.model.Transition; import java.util.List; import java.util.stream.Collectors; @@ -100,7 +101,8 @@ public final class AnalysisResultFinalizer { if (result.getMetadata() != null) { CodebaseMetadata metadata = result.getMetadata(); List triggers = canonicalizeTriggers(metadata.getTriggers(), context, machineTypes); - List callChains = canonicalizeCallChains(metadata.getCallChains(), context, machineTypes); + List callChains = canonicalizeCallChains( + metadata.getCallChains(), context, machineTypes, result.getTransitions()); result.setMetadata(CodebaseMetadata.builder() .triggers(triggers) .entryPoints(metadata.getEntryPoints()) @@ -126,7 +128,8 @@ public final class AnalysisResultFinalizer { private static List canonicalizeCallChains( List callChains, CodebaseContext context, - StateMachineTypeResolver.MachineTypes machineTypes) { + StateMachineTypeResolver.MachineTypes machineTypes, + List transitions) { if (callChains == null) { return null; } @@ -134,8 +137,16 @@ public final class AnalysisResultFinalizer { .map(chain -> { TriggerPoint trigger = chain.getTriggerPoint() == null ? null - : MachineEnumCanonicalizer.canonicalizeAndExpandTriggerPoint( - chain.getTriggerPoint(), machineTypes, context); + : MachineEnumCanonicalizer.prepareCallChainTrigger( + chain.getTriggerPoint(), + machineTypes, + context, + transitions, + chain.getPathBindings()); + if (trigger != null) { + trigger = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking( + trigger, machineTypes, context); + } List matched = canonicalizeMatchedTransitions( chain.getMatchedTransitions(), machineTypes, context); return chain.toBuilder() diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java index b8f08b9..2ae75ff 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java @@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.service; import click.kamil.springstatemachineexporter.analysis.model.CallEdge; import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; 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.CodebaseContext; import org.eclipse.jdt.core.dom.*; @@ -2008,7 +2009,7 @@ public final class EntryPointBindingExpander { if (constraint == null || constraint.isBlank() || context == null) { return constraint; } - List parts = splitTopLevelAnd(constraint); + List parts = ConstraintClauses.splitAndClauses(constraint); if (parts.isEmpty()) { return constraint; } @@ -2149,26 +2150,6 @@ public final class EntryPointBindingExpander { return findStaticImportHelper(helperName, compilationUnit, context); } - private static List splitTopLevelAnd(String constraint) { - List 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) { MethodDeclaration method = AstUtils.findEnclosingMethod(node); TypeDeclaration type = AstUtils.findEnclosingType(node); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstraintClausesTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstraintClausesTest.java index 0f57b42..4d82230 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstraintClausesTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstraintClausesTest.java @@ -65,4 +65,26 @@ class ConstraintClausesTest { assertThat(ConstraintClauses.carrierLiterals("message == \"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"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerBoundValueOfTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerBoundValueOfTest.java index b1f5abf..a595590 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerBoundValueOfTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerBoundValueOfTest.java @@ -90,7 +90,6 @@ class MachineEnumCanonicalizerBoundValueOfTest { trigger, machineTypes, null, - null, java.util.Map.of("message", "PAY")); assertThat(expanded.getPolymorphicEvents()) @@ -98,6 +97,31 @@ class MachineEnumCanonicalizerBoundValueOfTest { 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 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 void shouldExpandBoundValueOfFromObjectsEqualsConstraint() { TriggerPoint trigger = TriggerPoint.builder()