Narrow wide polymorphic events per call chain before linking.
Use multi-source endpoint evidence so shared dispatchers resolve to one event per endpoint without over-linking or fail-closed AMBIGUOUS_WIDEN. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,422 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.path.SymbolicPathValueEstimator;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Narrows wide call-graph polymorphic widens to endpoint-specific enum constants.
|
||||||
|
*
|
||||||
|
* <p>Uses multiple independent evidence sources (REST path, command keys, constraints, static trigger
|
||||||
|
* events, method-chain literals, camelCase method names) and only narrows when evidence converges.
|
||||||
|
*/
|
||||||
|
public final class CallChainPolyNarrower {
|
||||||
|
|
||||||
|
private static final int STRONG = 3;
|
||||||
|
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 ENUM_EQ = Pattern.compile(
|
||||||
|
"([A-Za-z_][\\w.]*)\\s*==\\s*([A-Za-z_][\\w.]*)");
|
||||||
|
private static final Pattern ROUTE_LITERAL = Pattern.compile(
|
||||||
|
"route\\(\\s*\"([^\"]+)\"\\s*,\\s*\"([^\"]+)\"\\s*\\)");
|
||||||
|
|
||||||
|
private CallChainPolyNarrower() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TriggerPoint narrowForCallChain(
|
||||||
|
TriggerPoint trigger,
|
||||||
|
CallChain chain,
|
||||||
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
|
List<Transition> machineTransitions,
|
||||||
|
CodebaseContext context) {
|
||||||
|
if (trigger == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
List<String> poly = trigger.getPolymorphicEvents();
|
||||||
|
if (poly == null || poly.size() <= 1) {
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
String eventTypeFqn = machineTypes.eventTypeFqn();
|
||||||
|
Set<String> configuredConstants = configuredEventConstants(machineTransitions, eventTypeFqn, context);
|
||||||
|
if (configuredConstants.isEmpty()) {
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
HintVotes votes = new HintVotes();
|
||||||
|
collectHints(trigger, chain, configuredConstants, context, votes);
|
||||||
|
|
||||||
|
String chosen = votes.choose(configuredConstants);
|
||||||
|
if (chosen == null) {
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> narrowed = filterPolyToConstant(poly, chosen, eventTypeFqn, context);
|
||||||
|
if (narrowed.size() == 1) {
|
||||||
|
return trigger.toBuilder()
|
||||||
|
.polymorphicEvents(narrowed)
|
||||||
|
.ambiguous(false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return trigger.toBuilder()
|
||||||
|
.polymorphicEvents(List.of(eventTypeFqn + "." + chosen))
|
||||||
|
.ambiguous(false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectHints(
|
||||||
|
TriggerPoint trigger,
|
||||||
|
CallChain chain,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
CodebaseContext context,
|
||||||
|
HintVotes votes) {
|
||||||
|
collectFromStaticTriggerEvent(trigger, configuredConstants, votes);
|
||||||
|
collectFromEntryPoint(chain != null ? chain.getEntryPoint() : null, configuredConstants, votes);
|
||||||
|
collectFromConstraint(trigger.getConstraint(), configuredConstants, votes);
|
||||||
|
if (chain != null) {
|
||||||
|
collectFromMethodChain(chain.getMethodChain(), configuredConstants, votes);
|
||||||
|
collectFromPathEstimator(chain, configuredConstants, context, votes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectFromStaticTriggerEvent(
|
||||||
|
TriggerPoint trigger,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
HintVotes votes) {
|
||||||
|
String event = trigger.getEvent();
|
||||||
|
if (event == null || event.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (MachineEnumCanonicalizer.isDynamicTriggerExpression(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addEnumReference(event, configuredConstants, votes, STRONG);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectFromEntryPoint(
|
||||||
|
EntryPoint entryPoint,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
HintVotes votes) {
|
||||||
|
if (entryPoint == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entryPoint.getName() != null) {
|
||||||
|
collectFromPath(entryPoint.getName(), configuredConstants, votes);
|
||||||
|
}
|
||||||
|
if (entryPoint.getMetadata() != null) {
|
||||||
|
Object path = entryPoint.getMetadata().get("path");
|
||||||
|
if (path instanceof String pathValue) {
|
||||||
|
collectFromPath(pathValue, configuredConstants, votes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entryPoint.getMethodName() != null) {
|
||||||
|
addUniqueIdentifierMatch(entryPoint.getMethodName(), configuredConstants, votes, WEAK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectFromPath(String rawPath, Set<String> configuredConstants, HintVotes votes) {
|
||||||
|
if (rawPath == null || rawPath.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int space = rawPath.indexOf(' ');
|
||||||
|
String path = space >= 0 ? rawPath.substring(space + 1).trim() : rawPath.trim();
|
||||||
|
if (path.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String segment : path.split("/")) {
|
||||||
|
if (segment.isBlank() || segment.contains("{")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addPathToken(segment, configuredConstants, votes, STRONG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectFromConstraint(
|
||||||
|
String constraint,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
HintVotes votes) {
|
||||||
|
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 ("event".equals(param) || "eventString".equals(param) || "actionKey".equals(param)
|
||||||
|
|| "action".equals(param)) {
|
||||||
|
addPathToken(literal, configuredConstants, votes, MEDIUM);
|
||||||
|
}
|
||||||
|
if ("commandKey".equals(param) || literal.contains(".")) {
|
||||||
|
addPathToken(literal, configuredConstants, votes, MEDIUM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Matcher route = ROUTE_LITERAL.matcher(constraint);
|
||||||
|
while (route.find()) {
|
||||||
|
addPathToken(route.group(2), configuredConstants, votes, MEDIUM);
|
||||||
|
}
|
||||||
|
Matcher enumEq = ENUM_EQ.matcher(constraint);
|
||||||
|
while (enumEq.find()) {
|
||||||
|
addEnumReference(enumEq.group(1), configuredConstants, votes, MEDIUM);
|
||||||
|
addEnumReference(enumEq.group(2), configuredConstants, votes, MEDIUM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectFromMethodChain(
|
||||||
|
List<String> methodChain,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
HintVotes votes) {
|
||||||
|
if (methodChain == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String methodFqn : methodChain) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
addUniqueIdentifierMatch(methodName, configuredConstants, votes, WEAK);
|
||||||
|
for (String token : splitCamelCase(methodName)) {
|
||||||
|
addPathToken(token, configuredConstants, votes, WEAK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectFromPathEstimator(
|
||||||
|
CallChain chain,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
CodebaseContext context,
|
||||||
|
HintVotes votes) {
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> estimated = new SymbolicPathValueEstimator().estimatePossibleValues(chain, context);
|
||||||
|
if (estimated == null || estimated.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int weight = estimated.size() == 1 ? MEDIUM : WEAK;
|
||||||
|
for (String value : estimated) {
|
||||||
|
addEnumReference(value, configuredConstants, votes, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPathToken(
|
||||||
|
String token,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
HintVotes votes,
|
||||||
|
int weight) {
|
||||||
|
if (token == null || token.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addPathTokenVariants(token, configuredConstants, votes, weight);
|
||||||
|
if (token.contains(".")) {
|
||||||
|
addPathTokenVariants(token.substring(token.lastIndexOf('.') + 1), configuredConstants, votes, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPathTokenVariants(
|
||||||
|
String token,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
HintVotes votes,
|
||||||
|
int weight) {
|
||||||
|
maybeVoteConstant(votes, normalizeToken(token), configuredConstants, weight);
|
||||||
|
for (String part : token.split("[-_.]")) {
|
||||||
|
if (!part.isBlank()) {
|
||||||
|
maybeVoteConstant(votes, normalizeToken(part), configuredConstants, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (token.toUpperCase(Locale.ROOT).contains("_")) {
|
||||||
|
String tail = token.substring(token.lastIndexOf('_') + 1);
|
||||||
|
maybeVoteConstant(votes, normalizeToken(tail), configuredConstants, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addEnumReference(
|
||||||
|
String reference,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
HintVotes votes,
|
||||||
|
int weight) {
|
||||||
|
if (reference == null || reference.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
maybeVoteConstant(votes, constantName(reference), configuredConstants, weight);
|
||||||
|
if (reference.contains(".")) {
|
||||||
|
maybeVoteConstant(votes, constantName(reference), configuredConstants, weight);
|
||||||
|
}
|
||||||
|
String upper = reference.toUpperCase(Locale.ROOT);
|
||||||
|
if (upper.contains("_")) {
|
||||||
|
maybeVoteConstant(votes, upper.substring(upper.lastIndexOf('_') + 1), configuredConstants, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addUniqueIdentifierMatch(
|
||||||
|
String identifier,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
HintVotes votes,
|
||||||
|
int weight) {
|
||||||
|
if (identifier == null || identifier.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String upper = identifier.toUpperCase(Locale.ROOT);
|
||||||
|
List<String> matched = new ArrayList<>();
|
||||||
|
for (String constant : configuredConstants) {
|
||||||
|
if (upper.contains(constant)) {
|
||||||
|
matched.add(constant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched.size() == 1) {
|
||||||
|
votes.add(matched.get(0), weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void maybeVoteConstant(
|
||||||
|
HintVotes votes,
|
||||||
|
String candidate,
|
||||||
|
Set<String> configuredConstants,
|
||||||
|
int weight) {
|
||||||
|
if (candidate == null || candidate.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String normalized = candidate.toUpperCase(Locale.ROOT);
|
||||||
|
if (configuredConstants.contains(normalized)) {
|
||||||
|
votes.add(normalized, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> filterPolyToConstant(
|
||||||
|
List<String> poly,
|
||||||
|
String constant,
|
||||||
|
String eventTypeFqn,
|
||||||
|
CodebaseContext context) {
|
||||||
|
List<String> narrowed = new ArrayList<>();
|
||||||
|
for (String candidate : poly) {
|
||||||
|
if (candidate == null || candidate.startsWith("<SYMBOLIC:") || candidate.startsWith("ENUM_SET:")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!constant.equalsIgnoreCase(constantName(candidate))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String canonical = candidate.contains(".")
|
||||||
|
? MachineEnumCanonicalizer.canonicalizeLabel(candidate, eventTypeFqn, context)
|
||||||
|
: eventTypeFqn + "." + constant;
|
||||||
|
if (!narrowed.contains(canonical)) {
|
||||||
|
narrowed.add(canonical);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return narrowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeToken(String token) {
|
||||||
|
return token.replace('-', '_').toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> splitCamelCase(String identifier) {
|
||||||
|
if (identifier == null || identifier.isBlank()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> tokens = new ArrayList<>();
|
||||||
|
StringBuilder current = new StringBuilder();
|
||||||
|
for (int i = 0; i < identifier.length(); i++) {
|
||||||
|
char ch = identifier.charAt(i);
|
||||||
|
if (Character.isUpperCase(ch) && !current.isEmpty()) {
|
||||||
|
tokens.add(current.toString());
|
||||||
|
current.setLength(0);
|
||||||
|
}
|
||||||
|
current.append(ch);
|
||||||
|
}
|
||||||
|
if (!current.isEmpty()) {
|
||||||
|
tokens.add(current.toString());
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> configuredEventConstants(
|
||||||
|
List<Transition> transitions,
|
||||||
|
String eventTypeFqn,
|
||||||
|
CodebaseContext context) {
|
||||||
|
Set<String> constants = new LinkedHashSet<>();
|
||||||
|
for (String event : MachineEnumCanonicalizer.polymorphicEventsFromTransitions(
|
||||||
|
transitions, eventTypeFqn, context)) {
|
||||||
|
constants.add(constantName(event).toUpperCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String constantName(String ref) {
|
||||||
|
int dot = ref.lastIndexOf('.');
|
||||||
|
return (dot >= 0 ? ref.substring(dot + 1) : ref).toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class HintVotes {
|
||||||
|
private final Map<String, Integer> scores = new HashMap<>();
|
||||||
|
private final Map<String, Integer> maxWeight = new HashMap<>();
|
||||||
|
|
||||||
|
void add(String constant, int weight) {
|
||||||
|
if (constant == null || constant.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = constant.toUpperCase(Locale.ROOT);
|
||||||
|
scores.merge(key, weight, Integer::sum);
|
||||||
|
maxWeight.merge(key, weight, Math::max);
|
||||||
|
}
|
||||||
|
|
||||||
|
String choose(Set<String> configured) {
|
||||||
|
String strongUnique = uniqueAboveWeight(configured, STRONG);
|
||||||
|
if (strongUnique != null) {
|
||||||
|
return strongUnique;
|
||||||
|
}
|
||||||
|
String mediumUnique = uniqueAboveWeight(configured, MEDIUM);
|
||||||
|
if (mediumUnique != null) {
|
||||||
|
return mediumUnique;
|
||||||
|
}
|
||||||
|
String best = null;
|
||||||
|
int bestScore = 0;
|
||||||
|
for (String constant : configured) {
|
||||||
|
int score = scores.getOrDefault(constant, 0);
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
best = constant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best == null || bestScore < MEDIUM) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final int winningScore = bestScore;
|
||||||
|
long contenders = scores.entrySet().stream()
|
||||||
|
.filter(e -> configured.contains(e.getKey()))
|
||||||
|
.filter(e -> e.getValue() == winningScore)
|
||||||
|
.count();
|
||||||
|
return contenders == 1 ? best : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String uniqueAboveWeight(Set<String> configured, int minWeight) {
|
||||||
|
String chosen = null;
|
||||||
|
for (String constant : configured) {
|
||||||
|
if (maxWeight.getOrDefault(constant, 0) >= minWeight) {
|
||||||
|
if (chosen != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
chosen = constant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chosen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
tp, machineTypes, context, chain.getEntryPoint());
|
tp, machineTypes, context, chain.getEntryPoint());
|
||||||
tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
||||||
tp, machineTypes, context, stateMachineTransitions, true);
|
tp, machineTypes, context, stateMachineTransitions, true);
|
||||||
|
tp = CallChainPolyNarrower.narrowForCallChain(
|
||||||
|
tp, chain, machineTypes, stateMachineTransitions, context);
|
||||||
tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, machineTypes, context);
|
tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, machineTypes, context);
|
||||||
tp = CallChainLinkPolicy.applyRestExternalPolicy(tp, chain, context);
|
tp = CallChainLinkPolicy.applyRestExternalPolicy(tp, chain, context);
|
||||||
if (machineTypes != null) {
|
if (machineTypes != null) {
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class CallChainPolyNarrowerTest {
|
||||||
|
|
||||||
|
private final TransitionLinkerEnricher enricher = new TransitionLinkerEnricher();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNarrowWidePolyUsingExpandedRestPathSegment() {
|
||||||
|
Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
|
||||||
|
Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.entryPoint(EntryPoint.builder()
|
||||||
|
.name("POST /api/machine/ORDER/transition/PAY")
|
||||||
|
.className("com.example.MachineController")
|
||||||
|
.methodName("transition")
|
||||||
|
.build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.MachineController.transition",
|
||||||
|
"com.example.StateMachineDispatcher.fireOrder",
|
||||||
|
"com.example.StateMachine.send"))
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.ambiguous(true)
|
||||||
|
.event("OrderEvent.valueOf(eventStr)")
|
||||||
|
.polymorphicEvents(List.of(
|
||||||
|
"com.example.OrderEvent.PAY",
|
||||||
|
"com.example.OrderEvent.SHIP",
|
||||||
|
"com.example.OrderEvent.CANCEL"))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfig")
|
||||||
|
.eventTypeFqn("com.example.OrderEvent")
|
||||||
|
.stateTypeFqn("com.example.OrderState")
|
||||||
|
.transitions(List.of(pay, ship))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
CallChain linked = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
|
||||||
|
assertThat(linked.getMatchedTransitions()).hasSize(1);
|
||||||
|
assertThat(linked.getMatchedTransitions().get(0).getEvent())
|
||||||
|
.isEqualTo("com.example.OrderEvent.PAY");
|
||||||
|
assertThat(linked.getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("com.example.OrderEvent.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNarrowWidePolyUsingRichDispatcherMethodName() {
|
||||||
|
Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
|
||||||
|
Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.entryPoint(EntryPoint.builder()
|
||||||
|
.name("POST /api/orders/rich/pay")
|
||||||
|
.className("com.example.RichOrderController")
|
||||||
|
.methodName("payViaRichEvent")
|
||||||
|
.build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.RichOrderController.payViaRichEvent",
|
||||||
|
"com.example.CommandGateway.payOrderViaRichEvent",
|
||||||
|
"com.example.CentralEventDispatcher.orderPayViaRichEvent",
|
||||||
|
"com.example.CentralEventDispatcher.sendOrderEvent"))
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.ambiguous(true)
|
||||||
|
.event("richEvent.getType()")
|
||||||
|
.polymorphicEvents(List.of(
|
||||||
|
"com.example.OrderEvent.PAY",
|
||||||
|
"com.example.OrderEvent.SHIP",
|
||||||
|
"com.example.OrderEvent.CANCEL"))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfig")
|
||||||
|
.eventTypeFqn("com.example.OrderEvent")
|
||||||
|
.stateTypeFqn("com.example.OrderState")
|
||||||
|
.transitions(List.of(pay, ship))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
CallChain linked = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
|
||||||
|
assertThat(linked.getMatchedTransitions()).hasSize(1);
|
||||||
|
assertThat(linked.getMatchedTransitions().get(0).getEvent())
|
||||||
|
.isEqualTo("com.example.OrderEvent.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNarrowUsingDottedCommandKeyPath() {
|
||||||
|
Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
|
||||||
|
Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.entryPoint(EntryPoint.builder()
|
||||||
|
.name("POST /api/commands/order.pay")
|
||||||
|
.className("com.example.GenericCommandController")
|
||||||
|
.methodName("execute")
|
||||||
|
.build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.GenericCommandController.execute",
|
||||||
|
"com.example.CommandGateway.executeViaMapper",
|
||||||
|
"com.example.CentralEventDispatcher.orderPay",
|
||||||
|
"com.example.CentralEventDispatcher.sendOrderEvent"))
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.ambiguous(true)
|
||||||
|
.event("OrderEvent.valueOf(actionKey)")
|
||||||
|
.constraint("\"order.pay\".equalsIgnoreCase(commandKey) && command == ORDER_PAY")
|
||||||
|
.polymorphicEvents(List.of(
|
||||||
|
"com.example.OrderEvent.PAY",
|
||||||
|
"com.example.OrderEvent.SHIP",
|
||||||
|
"com.example.OrderEvent.CANCEL"))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfig")
|
||||||
|
.eventTypeFqn("com.example.OrderEvent")
|
||||||
|
.stateTypeFqn("com.example.OrderState")
|
||||||
|
.transitions(List.of(pay, ship))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
CallChain linked = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
|
||||||
|
assertThat(linked.getMatchedTransitions()).hasSize(1);
|
||||||
|
assertThat(linked.getMatchedTransitions().get(0).getEvent())
|
||||||
|
.isEqualTo("com.example.OrderEvent.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailClosedWhenWidePolyHasNoEndpointSpecificHint() {
|
||||||
|
Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
|
||||||
|
Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.entryPoint(EntryPoint.builder()
|
||||||
|
.name("POST /api/machine/{machineType}/transition/{event}")
|
||||||
|
.className("com.example.MachineController")
|
||||||
|
.methodName("transition")
|
||||||
|
.build())
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.ambiguous(true)
|
||||||
|
.external(true)
|
||||||
|
.event("OrderEvent.valueOf(eventStr)")
|
||||||
|
.polymorphicEvents(List.of(
|
||||||
|
"com.example.OrderEvent.PAY",
|
||||||
|
"com.example.OrderEvent.SHIP"))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfig")
|
||||||
|
.eventTypeFqn("com.example.OrderEvent")
|
||||||
|
.stateTypeFqn("com.example.OrderState")
|
||||||
|
.transitions(List.of(pay, ship))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
enricher.enrich(result, null, null);
|
||||||
|
|
||||||
|
CallChain linked = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
|
||||||
|
assertThat(linked.getLinkResolution()).isIn(
|
||||||
|
LinkResolution.AMBIGUOUS_WIDEN,
|
||||||
|
LinkResolution.UNRESOLVED_EXTERNAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Transition transition(String source, String target, String eventFqn) {
|
||||||
|
Transition transition = new Transition();
|
||||||
|
transition.setSourceStates(List.of(State.of(source, "com.example.OrderState." + source)));
|
||||||
|
transition.setTargetStates(List.of(State.of(target, "com.example.OrderState." + target)));
|
||||||
|
transition.setEvent(Event.of(eventFqn.substring(eventFqn.lastIndexOf('.') + 1), eventFqn));
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,14 +67,12 @@ class DispatcherPolyCeilingPipelineTest {
|
|||||||
CallChain linked = linkChain(
|
CallChain linked = linkChain(
|
||||||
context, bloatedChain, MACHINE_CONFIG, EVENT_TYPE, STATE_TYPE, pay, ship);
|
context, bloatedChain, MACHINE_CONFIG, EVENT_TYPE, STATE_TYPE, pay, ship);
|
||||||
|
|
||||||
assertPolyWithinTransitions(linked, 2);
|
|
||||||
assertPolyCappedToTransitionEvents(linked, EVENT_TYPE, pay, ship);
|
|
||||||
assertThat(linked.getTriggerPoint().getPolymorphicEvents())
|
assertThat(linked.getTriggerPoint().getPolymorphicEvents())
|
||||||
.containsExactlyInAnyOrder(EVENT_TYPE + ".PAY", EVENT_TYPE + ".SHIP");
|
.containsExactly(EVENT_TYPE + ".PAY");
|
||||||
assertMatchedWithinTransitions(linked, 2);
|
assertMatchedWithinTransitions(linked, 1);
|
||||||
assertThat(linked.getMatchedTransitions())
|
assertThat(linked.getMatchedTransitions())
|
||||||
.extracting(MatchedTransition::getEvent)
|
.extracting(MatchedTransition::getEvent)
|
||||||
.containsExactlyInAnyOrder(EVENT_TYPE + ".PAY", EVENT_TYPE + ".SHIP");
|
.containsExactly(EVENT_TYPE + ".PAY");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user