diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java index b0a12bf..f37e1e8 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainEnricher.java @@ -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 chains = buildCallChainsForMachine(result, context, intelligence); + List 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 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 scopedTriggers = MachineScopeFilter.filterTriggersForMachine( - intelligence.findTriggerPoints().stream() - .map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes)) - .collect(Collectors.toList()), - result.getName(), - context); + List scopedTriggers = null; + if (preferMetadataTriggers) { + scopedTriggers = result.getMetadata().getTriggers(); + } + if (scopedTriggers == null || scopedTriggers.isEmpty()) { + scopedTriggers = MachineScopeFilter.filterTriggersForMachine( + intelligence.findTriggerPoints(), + result.getName(), + context); + } List 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); } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/MachineScopeFilter.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/MachineScopeFilter.java index 1be184f..c9b9732 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/MachineScopeFilter.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/MachineScopeFilter.java @@ -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 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) { 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 d3089a2..67f36bb 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 @@ -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 updatedChains = new ArrayList<>(); List 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 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 simplifyCache = new java.util.HashMap<>(); - private final java.util.Map, 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 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; - } - } \ No newline at end of file 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 7dbf74e..9eea440 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 @@ -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(); - } } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java index be3abd1..59f160b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/matching/StrictFqnMatchingEngine.java @@ -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") || diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java index 146ff22..3b25eab 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java @@ -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 paramValues, CodebaseContext context, Set visited) { - String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues, context); + String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues, context, visited); if (switchVar == null) { Set 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 paramValues, CodebaseContext context, Set visited) { - String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues, context); + String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues, context, visited); if (switchVar == null) { Set 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 paramValues, CodebaseContext context) { + private String resolveExpressionWithParams( + Expression expr, java.util.Map paramValues, CodebaseContext context, Set 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 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); } 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 8fff263..cee0c94 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 @@ -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 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 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; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JsonExportContextFactory.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JsonExportContextFactory.java index 83195cf..7ae0e44 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JsonExportContextFactory.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JsonExportContextFactory.java @@ -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 activeProfiles) throws IOException { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java index 603826c..328c4b1 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java @@ -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()) { diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/DispatcherExternalFalseMatchedTransitionsRegressionTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/DispatcherExternalFalseMatchedTransitionsRegressionTest.java index ae621c0..3b46fe3 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/DispatcherExternalFalseMatchedTransitionsRegressionTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/DispatcherExternalFalseMatchedTransitionsRegressionTest.java @@ -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())