Fix JSON re-export StackOverflow and dedupe enricher canonicalization.

Scope project-root discovery to the nearest module, skip call-chain refresh when source lacks machine classes, harden ConstantResolver cycle detection, and remove redundant trigger canonicalization passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 15:39:48 +02:00
parent 58d0701df5
commit 873e2c91f3
10 changed files with 107 additions and 186 deletions

View File

@@ -5,14 +5,11 @@ 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.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class CallChainEnricher implements AnalysisEnricher {
@@ -20,7 +17,7 @@ public class CallChainEnricher implements AnalysisEnricher {
@Override
public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
log.info("Enriching {} with call chains", result.getName());
List<CallChain> chains = buildCallChainsForMachine(result, context, intelligence);
List<CallChain> chains = buildCallChainsForMachine(result, context, intelligence, true);
if (chains == null) {
return;
}
@@ -38,6 +35,14 @@ public class CallChainEnricher implements AnalysisEnricher {
AnalysisResult result,
CodebaseContext context,
CodebaseIntelligenceProvider intelligence) {
return buildCallChainsForMachine(result, context, intelligence, false);
}
public static List<CallChain> buildCallChainsForMachine(
AnalysisResult result,
CodebaseContext context,
CodebaseIntelligenceProvider intelligence,
boolean preferMetadataTriggers) {
if (result.getMetadata() == null || intelligence == null) {
return null;
}
@@ -46,25 +51,18 @@ public class CallChainEnricher implements AnalysisEnricher {
return null;
}
StateMachineTypeResolver.MachineTypes machineTypes =
StateMachineTypeResolver.resolveTypes(result.getName(), context);
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
intelligence.findTriggerPoints().stream()
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
.collect(Collectors.toList()),
result.getName(),
context);
List<TriggerPoint> scopedTriggers = null;
if (preferMetadataTriggers) {
scopedTriggers = result.getMetadata().getTriggers();
}
if (scopedTriggers == null || scopedTriggers.isEmpty()) {
scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
intelligence.findTriggerPoints(),
result.getName(),
context);
}
List<CallChain> chains = intelligence.findCallChains(entryPoints, scopedTriggers);
chains = chains.stream()
.map(chain -> chain.getTriggerPoint() == null
? chain
: chain.toBuilder()
.triggerPoint(MachineEnumCanonicalizer.canonicalizeTriggerPoint(
chain.getTriggerPoint(), machineTypes))
.build())
.collect(Collectors.toList());
return MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
}
}

View File

@@ -54,7 +54,7 @@ public final class MachineScopeFilter {
if (entryPoints == null || machineName == null) {
return entryPoints == null ? List.of() : entryPoints;
}
String machineDomain = extractMachineDomainKey(machineName);
String machineDomain = MachineDomainKeys.extractMachineDomainKey(machineName);
List<EntryPoint> filtered = new ArrayList<>();
for (EntryPoint entryPoint : entryPoints) {
if (isEntryPointForMachine(entryPoint, machineDomain, machineName, context)) {
@@ -92,8 +92,7 @@ public final class MachineScopeFilter {
}
private static boolean isKnownDomainScopedMachine(String machineDomain) {
String normalized = normalizeDomainKey(machineDomain);
return "ORDER".equals(normalized) || "DOCUMENT".equals(normalized) || "USER".equals(normalized);
return "ORDER".equals(machineDomain) || "DOCUMENT".equals(machineDomain) || "USER".equals(machineDomain);
}
private static String inferDomainFromPath(String path) {
@@ -108,38 +107,12 @@ public final class MachineScopeFilter {
}
private static boolean domainsMatch(String pathDomain, String machineDomain) {
String normalizedPath = normalizeDomainKey(pathDomain);
String normalizedMachine = normalizeDomainKey(machineDomain);
return normalizedPath.equalsIgnoreCase(normalizedMachine)
|| normalizedMachine.startsWith(normalizedPath)
|| normalizedPath.startsWith(normalizedMachine);
}
private static String extractMachineDomainKey(String machineName) {
String simple = machineName.substring(machineName.lastIndexOf('.') + 1);
String upper = simple.toUpperCase(Locale.ROOT);
for (String marker : List.of("STATEMACHINECONFIGURATION", "STATEMACHINE", "CONFIGURATION", "CONFIG")) {
int idx = upper.indexOf(marker);
if (idx > 0) {
return normalizeDomainKey(upper.substring(0, idx));
}
if (pathDomain == null || machineDomain == null) {
return false;
}
int stateIdx = upper.indexOf("STATE");
if (stateIdx > 0) {
return normalizeDomainKey(upper.substring(0, stateIdx));
}
return normalizeDomainKey(upper);
}
private static String normalizeDomainKey(String domain) {
if (domain == null || domain.isEmpty()) {
return domain;
}
String upper = domain.toUpperCase(Locale.ROOT);
if (upper.startsWith("STANDARD") && upper.length() > "STANDARD".length()) {
return upper.substring("STANDARD".length());
}
return upper;
return pathDomain.equalsIgnoreCase(machineDomain)
|| machineDomain.startsWith(pathDomain)
|| pathDomain.startsWith(machineDomain);
}
private static boolean isTriggerForMachine(TriggerPoint trigger, String machineName, CodebaseContext context) {

View File

@@ -8,13 +8,13 @@ import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMar
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import click.kamil.springstatemachineexporter.analysis.enricher.matching.EventMatchingEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine;
@@ -38,12 +38,11 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
List<CallChain> updatedChains = new ArrayList<>();
List<Transition> stateMachineTransitions = result.getTransitions();
StateMachineTypeResolver.MachineTypes machineTypes =
StateMachineTypeResolver.resolveTypes(result.getName(), context);
if (machineTypes.stateTypeFqn() == null && machineTypes.eventTypeFqn() == null
&& (result.getStateTypeFqn() != null || result.getEventTypeFqn() != null)) {
machineTypes = new StateMachineTypeResolver.MachineTypes(
result.getStateTypeFqn(), result.getEventTypeFqn());
}
JsonExportContextFactory.resolveMachineTypes(
result.getName(),
result.getStateTypeFqn(),
result.getEventTypeFqn(),
context);
for (CallChain chain : result.getMetadata().getCallChains()) {
TriggerPoint tp = chain.getTriggerPoint();
@@ -53,7 +52,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
}
tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
tp, machineTypes, context, stateMachineTransitions);
tp, machineTypes, context, stateMachineTransitions, true);
tp = markRestEndpointExternal(tp, chain);
if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) {
@@ -61,7 +60,6 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
continue;
}
String triggerEvent = simplify(tp.getEvent());
String triggerSource = tp.getSourceState() != null ? simplify(tp.getSourceState()) : null;
List<MatchedTransition> matched = new ArrayList<>();
@@ -141,30 +139,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
return true;
}
return BooleanConstraintEvaluator.isCompatibleWithMachineDomain(
constraint, extractMachineDomainKey(machineName));
}
private String extractMachineDomainKey(String machineName) {
String simple = machineName.substring(machineName.lastIndexOf('.') + 1);
String upper = simple.toUpperCase();
for (String marker : List.of("STATEMACHINECONFIGURATION", "STATEMACHINE", "CONFIGURATION", "CONFIG")) {
int idx = upper.indexOf(marker);
if (idx > 0) {
return upper.substring(0, idx);
}
}
int stateIdx = upper.indexOf("STATE");
if (stateIdx > 0) {
return upper.substring(0, stateIdx);
}
return upper;
constraint, MachineDomainKeys.extractMachineDomainKey(machineName));
}
private final java.util.Map<String, String> simplifyCache = new java.util.HashMap<>();
private final java.util.Map<java.util.Map.Entry<String, String>, Boolean> guardedCache = new java.util.HashMap<>();
private static final java.util.regex.Pattern SUFFIX_PATTERN = java.util.regex.Pattern.compile("(?i)(.+)(Event|Action|Transition|Command)(s)?$");
private static final java.util.regex.Pattern FQN_PATTERN = java.util.regex.Pattern.compile("^.*\\.([A-Z0-9_]+)$");
@@ -217,42 +195,4 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
return trigger.toBuilder().external(true).build();
}
private boolean isGuardedContains(String smEvent, String triggerEvent) {
if (smEvent == null || triggerEvent == null) return false;
// Use a strictly immutable pair to guarantee 100% collision-free caching regardless of string content
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(smEvent, triggerEvent);
if (guardedCache.containsKey(cacheKey)) return guardedCache.get(cacheKey);
boolean result = calculateGuardedContains(smEvent, triggerEvent);
guardedCache.put(cacheKey, result);
return result;
}
private boolean calculateGuardedContains(String smEvent, String triggerEvent) {
// Only fire if it looks like a simple identifier (no spaces, no parens)
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
triggerEvent.contains(" ") || triggerEvent.contains("(") || triggerEvent.contains(")")) {
return false;
}
String shorter = smEvent.length() < triggerEvent.length() ? smEvent : triggerEvent;
String longer = smEvent.length() >= triggerEvent.length() ? smEvent : triggerEvent;
shorter = shorter.toLowerCase();
longer = longer.toLowerCase();
if (longer.contains(shorter)) {
// Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators
// to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE")
if (shorter.length() < 4) {
return longer.matches(".*(^|_|-)" + java.util.regex.Pattern.quote(shorter) + "(_|-|$).*");
}
// For longer strings, a straight contains is usually safe enough
return true;
}
return false;
}
}

View File

@@ -7,6 +7,7 @@ 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.analysis.service.CodebaseIntelligenceProvider;
import click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
@@ -26,7 +27,8 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
return;
}
StateMachineTypeResolver.MachineTypes machineTypes = resolveMachineTypes(result, context);
StateMachineTypeResolver.MachineTypes machineTypes = JsonExportContextFactory.resolveMachineTypes(
result.getName(), result.getStateTypeFqn(), result.getEventTypeFqn(), context);
if (machineTypes.stateTypeFqn() == null && machineTypes.eventTypeFqn() == null) {
return;
}
@@ -63,18 +65,4 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
.properties(result.getMetadata().getProperties())
.build());
}
private static StateMachineTypeResolver.MachineTypes resolveMachineTypes(
AnalysisResult result, CodebaseContext context) {
StateMachineTypeResolver.MachineTypes resolved =
StateMachineTypeResolver.resolveTypes(result.getName(), context);
if (resolved.stateTypeFqn() != null || resolved.eventTypeFqn() != null) {
return resolved;
}
if (result.getStateTypeFqn() != null || result.getEventTypeFqn() != null) {
return new StateMachineTypeResolver.MachineTypes(
result.getStateTypeFqn(), result.getEventTypeFqn());
}
return StateMachineTypeResolver.MachineTypes.empty();
}
}

View File

@@ -1,6 +1,7 @@
package click.kamil.springstatemachineexporter.analysis.enricher.matching;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.model.Event;
import java.util.List;
@@ -51,7 +52,7 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
if (pe.equals(smConst) && triggerPoint.getEventTypeFqn() != null
&& !isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
String smEnumType = smEventRaw.substring(0, smEventRaw.lastIndexOf('.'));
return enumTypesMatch(triggerPoint.getEventTypeFqn(), smEnumType);
return MachineEnumCanonicalizer.enumTypesMatch(triggerPoint.getEventTypeFqn(), smEnumType);
}
}
}
@@ -99,12 +100,12 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
String smEnumType = smEvent.substring(0, smEvent.lastIndexOf('.'));
if (eventTypeFqn != null) {
return enumTypesMatch(eventTypeFqn, smEnumType);
return MachineEnumCanonicalizer.enumTypesMatch(eventTypeFqn, smEnumType);
}
if (triggerEvent.contains(".")) {
String triggerEnumType = triggerEvent.substring(0, triggerEvent.lastIndexOf('.'));
return enumTypesMatch(triggerEnumType, smEnumType);
return MachineEnumCanonicalizer.enumTypesMatch(triggerEnumType, smEnumType);
}
// Bare constant name (e.g. after Enum.name()) with no type context must not match every enum
@@ -119,24 +120,6 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
return event.contains(".") ? event.substring(event.lastIndexOf('.') + 1) : event;
}
private boolean enumTypesMatch(String type1, String type2) {
if (type1 == null || type2 == null) return false;
if (type1.equals(type2)) return true;
if (type1.endsWith("." + type2) || type2.endsWith("." + type1)) return true;
String simple1 = simpleName(type1);
String simple2 = simpleName(type2);
if (!simple1.equals(simple2)) return false;
// Same simple name only matches when at least one side is package-less (import-style reference).
// Prevents com.foo.OrderEvents from matching com.bar.OrderEvents.
return !type1.contains(".") || !type2.contains(".");
}
private String simpleName(String name) {
return name.contains(".") ? name.substring(name.lastIndexOf('.') + 1) : name;
}
private boolean isStringTypeOrPrimitive(String typeFqn) {
if (typeFqn == null) return false;
return typeFqn.equals("String") || typeFqn.equals("java.lang.String") ||

View File

@@ -345,7 +345,7 @@ public class ConstantResolver {
String varName = fragment.getName().getIdentifier();
declaredLocals.add(varName);
if (fragment.getInitializer() != null) {
String rhsVal = resolveExpressionWithParams(fragment.getInitializer(), localVars, context);
String rhsVal = resolveExpressionWithParams(fragment.getInitializer(), localVars, context, visited);
if (rhsVal == null) rhsVal = resolveInternal(fragment.getInitializer(), context, visited);
if (rhsVal != null) {
localVars.put(varName, rhsVal);
@@ -380,7 +380,7 @@ public class ConstantResolver {
}
for (int i = 0; i < node.arguments().size() && i < sideEffectMd.parameters().size(); i++) {
Expression arg = (Expression) node.arguments().get(i);
String val = resolveExpressionWithParams(arg, localVars, context);
String val = resolveExpressionWithParams(arg, localVars, context, visited);
if (val == null) val = resolveInternal(arg, context, visited);
if (val != null) {
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) sideEffectMd.parameters().get(i);
@@ -397,7 +397,7 @@ public class ConstantResolver {
}
}
}
return super.visit(node);
return false;
}
@Override
@@ -410,7 +410,7 @@ public class ConstantResolver {
varName = "this." + fa.getName().getIdentifier();
}
if (varName != null) {
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars, context);
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars, context, visited);
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
if (rhsVal != null) {
localVars.put(varName, rhsVal);
@@ -450,7 +450,7 @@ public class ConstantResolver {
String result = evaluateSwitchExpression(se, localVars, context, visited);
if (result != null) finalResult[0] = result;
} else if (rs.getExpression() != null) {
String result = resolveExpressionWithParams(rs.getExpression(), localVars, context);
String result = resolveExpressionWithParams(rs.getExpression(), localVars, context, visited);
if (result == null) result = resolveInternal(rs.getExpression(), context, visited);
if (result != null) finalResult[0] = result;
}
@@ -463,7 +463,7 @@ public class ConstantResolver {
}
private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues, context);
String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues, context, visited);
if (switchVar == null) {
Set<String> values = new LinkedHashSet<>();
for (Object stmtObj : ss.statements()) {
@@ -515,7 +515,7 @@ public class ConstantResolver {
matchingCase = true;
} else {
for (Object exprObj : sc.expressions()) {
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues, context);
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues, context, visited);
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
caseVal = caseSn.getIdentifier();
@@ -550,7 +550,7 @@ public class ConstantResolver {
}
private String evaluateSwitchExpression(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues, context);
String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues, context, visited);
if (switchVar == null) {
Set<String> values = new LinkedHashSet<>();
for (Object stmtObj : se.statements()) {
@@ -602,7 +602,7 @@ public class ConstantResolver {
matchingCase = true;
} else {
for (Object exprObj : sc.expressions()) {
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues, context);
String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues, context, visited);
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
caseVal = caseSn.getIdentifier();
@@ -636,7 +636,8 @@ public class ConstantResolver {
return null;
}
private String resolveExpressionWithParams(Expression expr, java.util.Map<String, String> paramValues, CodebaseContext context) {
private String resolveExpressionWithParams(
Expression expr, java.util.Map<String, String> paramValues, CodebaseContext context, Set<String> visited) {
if (expr instanceof SimpleName sn) {
String name = sn.getIdentifier();
if (paramValues.containsKey(name)) return paramValues.get(name);
@@ -658,7 +659,7 @@ public class ConstantResolver {
arg = (Expression) mi.arguments().get(1);
}
if (arg != null) {
String resolvedArg = resolveExpressionWithParams(arg, paramValues, context);
String resolvedArg = resolveExpressionWithParams(arg, paramValues, context, visited);
if (resolvedArg != null && resolvedArg.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
String enumFqn = null;
if (mi.arguments().size() == 2) {
@@ -705,14 +706,14 @@ public class ConstantResolver {
}
for (int i = 0; i < mi.arguments().size() && i < sideEffectMd.parameters().size(); i++) {
Expression arg = (Expression) mi.arguments().get(i);
String val = resolveExpressionWithParams(arg, paramValues, context);
if (val == null) val = resolveInternal(arg, context, new java.util.HashSet<>());
String val = resolveExpressionWithParams(arg, paramValues, context, visited);
if (val == null) val = resolveInternal(arg, context, visited);
if (val != null) {
org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) sideEffectMd.parameters().get(i);
inlineLocals.put(param.getName().getIdentifier(), val);
}
}
String ret = evaluateMethodBodyWithLocals(sideEffectMd, inlineLocals, context, new java.util.HashSet<>());
String ret = evaluateMethodBodyWithLocals(sideEffectMd, inlineLocals, context, visited);
for (java.util.Map.Entry<String, String> entry : inlineLocals.entrySet()) {
if (entry.getKey().startsWith("this.")) {
paramValues.put(entry.getKey(), entry.getValue());
@@ -736,7 +737,7 @@ public class ConstantResolver {
if (!"build".equals(methodName) && !"builder".equals(methodName) && !"toBuilder".equals(methodName)) {
if (current.arguments().size() == 1) {
Expression arg = (Expression) current.arguments().get(0);
String val = resolveExpressionWithParams(arg, localVars, context);
String val = resolveExpressionWithParams(arg, localVars, context, visited);
if (val == null) val = resolveInternal(arg, context, visited);
if (val != null) fields.put(methodName, val);
}

View File

@@ -161,7 +161,7 @@ public final class MachineEnumCanonicalizer {
TriggerPoint trigger,
StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context) {
return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, null);
return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, null, false);
}
public static TriggerPoint ensureCallChainPolymorphicEvents(
@@ -169,7 +169,18 @@ public final class MachineEnumCanonicalizer {
StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context,
List<Transition> machineTransitions) {
TriggerPoint expanded = canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, machineTransitions, false);
}
public static TriggerPoint ensureCallChainPolymorphicEvents(
TriggerPoint trigger,
StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context,
List<Transition> machineTransitions,
boolean skipCanonicalization) {
TriggerPoint expanded = skipCanonicalization
? trigger
: canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
if (expanded == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
return expanded;
}
@@ -543,7 +554,7 @@ public final class MachineEnumCanonicalizer {
return simple.equals(typePart) || (simple + "s").equals(typePart);
}
static boolean enumTypesMatch(String type1, String type2) {
public static boolean enumTypesMatch(String type1, String type2) {
if (type1 == null || type2 == null) {
return false;
}

View File

@@ -16,21 +16,42 @@ public final class JsonExportContextFactory {
private JsonExportContextFactory() {
}
/**
* Returns the nearest ancestor directory that looks like a Gradle/Maven module
* ({@code build.gradle} or {@code pom.xml}), not the monorepo {@code settings.gradle} root.
*/
public static Path findProjectRoot(Path start) {
Path current = start != null ? start.toAbsolutePath().normalize() : null;
Path settingsRoot = null;
Path buildRoot = null;
while (current != null) {
if (settingsRoot == null && Files.exists(current.resolve("settings.gradle"))) {
settingsRoot = current;
}
if (buildRoot == null && (Files.exists(current.resolve("pom.xml"))
|| Files.exists(current.resolve("build.gradle")))) {
buildRoot = current;
if (Files.exists(current.resolve("build.gradle")) || Files.exists(current.resolve("pom.xml"))) {
return current;
}
current = current.getParent();
}
return settingsRoot != null ? settingsRoot : buildRoot;
return null;
}
/** True when the scanned context actually contains classes referenced by the imported JSON. */
public static boolean contextContainsAnalysisSources(
click.kamil.springstatemachineexporter.analysis.model.AnalysisResult result,
CodebaseContext context) {
if (context == null || result == null) {
return false;
}
if (result.getName() != null && context.getTypeDeclaration(result.getName()) != null) {
return true;
}
if (result.getMetadata() == null || result.getMetadata().getEntryPoints() == null) {
return false;
}
for (click.kamil.springstatemachineexporter.analysis.model.EntryPoint entryPoint
: result.getMetadata().getEntryPoints()) {
if (entryPoint.getClassName() != null
&& context.getTypeDeclaration(entryPoint.getClassName()) != null) {
return true;
}
}
return false;
}
public static CodebaseContext scanProjectRoot(Path projectRoot, List<String> activeProfiles) throws IOException {

View File

@@ -193,8 +193,13 @@ public class ExportService {
result.getStateTypeFqn(),
result.getEventTypeFqn(),
context);
// Canonicalize transitions/states for relink matching; trigger/callChain canonicalization
// here is superseded by refreshCallChainsAndRelink / relinkAfterPropertyResolution
// (TriggerCanonicalizationEnricher runs again before TransitionLinkerEnricher).
AnalysisResultFinalizer.applyCanonicalization(result, context, machineTypes);
if (context != null && sourceRoot != null
&& click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
.contextContainsAnalysisSources(result, context)
&& result.getMetadata() != null
&& result.getMetadata().getEntryPoints() != null
&& !result.getMetadata().getEntryPoints().isEmpty()) {

View File

@@ -77,6 +77,7 @@ class DispatcherExternalFalseMatchedTransitionsRegressionTest {
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
new TriggerCanonicalizationEnricher().enrich(result, null, null);
enricher.enrich(result, null, null);
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions())