Fix empty matchedTransitions for dynamic dispatcher call chains.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ 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.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
@@ -19,20 +20,43 @@ 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);
|
||||
if (chains == null) {
|
||||
return;
|
||||
}
|
||||
result.addMetadata(CodebaseMetadata.builder()
|
||||
.callChains(chains)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-resolves call chains from the codebase call graph for the given machine.
|
||||
* Used on JSON re-export when source is available to refresh {@code polymorphicEvents},
|
||||
* {@code external}, and branch {@code constraint} on trigger points.
|
||||
*/
|
||||
public static List<CallChain> buildCallChainsForMachine(
|
||||
AnalysisResult result,
|
||||
CodebaseContext context,
|
||||
CodebaseIntelligenceProvider intelligence) {
|
||||
if (result.getMetadata() == null || intelligence == null) {
|
||||
return null;
|
||||
}
|
||||
List<EntryPoint> entryPoints = result.getMetadata().getEntryPoints();
|
||||
if (entryPoints == null || entryPoints.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||
StateMachineTypeResolver.resolveTypes(result.getName(), context);
|
||||
|
||||
List<TriggerPoint> canonicalTriggers = intelligence.findTriggerPoints().stream()
|
||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||
.collect(Collectors.toList());
|
||||
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
|
||||
canonicalTriggers, result.getName(), context);
|
||||
intelligence.findTriggerPoints().stream()
|
||||
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||
.collect(Collectors.toList()),
|
||||
result.getName(),
|
||||
context);
|
||||
|
||||
List<CallChain> chains = intelligence.findCallChains(
|
||||
result.getMetadata().getEntryPoints(),
|
||||
scopedTriggers
|
||||
);
|
||||
List<CallChain> chains = intelligence.findCallChains(entryPoints, scopedTriggers);
|
||||
chains = chains.stream()
|
||||
.map(chain -> chain.getTriggerPoint() == null
|
||||
? chain
|
||||
@@ -41,10 +65,6 @@ public class CallChainEnricher implements AnalysisEnricher {
|
||||
chain.getTriggerPoint(), machineTypes))
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
chains = MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
||||
|
||||
result.addMetadata(CodebaseMetadata.builder()
|
||||
.callChains(chains)
|
||||
.build());
|
||||
return MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictF
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
|
||||
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
|
||||
@@ -35,6 +37,13 @@ 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());
|
||||
}
|
||||
|
||||
for (CallChain chain : result.getMetadata().getCallChains()) {
|
||||
TriggerPoint tp = chain.getTriggerPoint();
|
||||
@@ -43,6 +52,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
continue;
|
||||
}
|
||||
|
||||
tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
||||
tp, machineTypes, context, stateMachineTransitions);
|
||||
tp = markRestEndpointExternal(tp, chain);
|
||||
|
||||
if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) {
|
||||
updatedChains.add(chain);
|
||||
continue;
|
||||
@@ -90,10 +103,16 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
|
||||
if (!matched.isEmpty()) {
|
||||
CallChain newChain = chain.toBuilder().matchedTransitions(matched).build();
|
||||
CallChain newChain = chain.toBuilder()
|
||||
.triggerPoint(tp)
|
||||
.matchedTransitions(matched)
|
||||
.build();
|
||||
updatedChains.add(newChain);
|
||||
} else {
|
||||
updatedChains.add(chain);
|
||||
updatedChains.add(chain.toBuilder()
|
||||
.triggerPoint(tp)
|
||||
.matchedTransitions(null)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +198,25 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
return event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
|
||||
}
|
||||
|
||||
private TriggerPoint markRestEndpointExternal(TriggerPoint trigger, CallChain chain) {
|
||||
if (trigger == null || trigger.isExternal() || chain.getEntryPoint() == null) {
|
||||
return trigger;
|
||||
}
|
||||
EntryPoint entryPoint = chain.getEntryPoint();
|
||||
if (entryPoint.getType() != EntryPoint.Type.REST) {
|
||||
return trigger;
|
||||
}
|
||||
String endpointName = entryPoint.getName();
|
||||
if (endpointName == null || !endpointName.contains("{")) {
|
||||
return trigger;
|
||||
}
|
||||
if (MachineEnumCanonicalizer.classifyTriggerEvent(trigger.getEvent())
|
||||
!= MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION) {
|
||||
return trigger;
|
||||
}
|
||||
return trigger.toBuilder().external(true).build();
|
||||
}
|
||||
|
||||
private boolean isGuardedContains(String smEvent, String triggerEvent) {
|
||||
if (smEvent == null || triggerEvent == null) return false;
|
||||
|
||||
|
||||
@@ -152,6 +152,92 @@ public final class MachineEnumCanonicalizer {
|
||||
return canonical.toBuilder().polymorphicEvents(expanded).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* When a call-chain trigger uses a dynamic fire-site expression ({@code valueOf}, variable, getter)
|
||||
* but has no concrete {@code polymorphicEvents}, infer all machine enum events so transition linking
|
||||
* can proceed. Used only for {@code callChains[].triggerPoint}, not {@code metadata.triggers}.
|
||||
*/
|
||||
public static TriggerPoint ensureCallChainPolymorphicEvents(
|
||||
TriggerPoint trigger,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||
CodebaseContext context) {
|
||||
return ensureCallChainPolymorphicEvents(trigger, machineTypes, context, null);
|
||||
}
|
||||
|
||||
public static TriggerPoint ensureCallChainPolymorphicEvents(
|
||||
TriggerPoint trigger,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions) {
|
||||
TriggerPoint expanded = canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
|
||||
if (expanded == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
||||
return expanded;
|
||||
}
|
||||
if (hasConcretePolymorphicEvents(expanded.getPolymorphicEvents())) {
|
||||
return expanded;
|
||||
}
|
||||
if (classifyTriggerEvent(expanded.getEvent()) != TriggerEventKind.DYNAMIC_EXPRESSION) {
|
||||
return expanded;
|
||||
}
|
||||
List<String> machineEvents = allPackageCanonicalEnumConstants(machineTypes.eventTypeFqn(), context);
|
||||
if (machineEvents.isEmpty()) {
|
||||
machineEvents = polymorphicEventsFromTransitions(machineTransitions, machineTypes.eventTypeFqn());
|
||||
}
|
||||
if (machineEvents.isEmpty()) {
|
||||
return expanded;
|
||||
}
|
||||
return expanded.toBuilder().polymorphicEvents(machineEvents).ambiguous(true).build();
|
||||
}
|
||||
|
||||
static List<String> polymorphicEventsFromTransitions(
|
||||
List<Transition> machineTransitions,
|
||||
String eventTypeFqn) {
|
||||
if (machineTransitions == null || machineTransitions.isEmpty() || eventTypeFqn == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> events = new ArrayList<>();
|
||||
for (Transition transition : machineTransitions) {
|
||||
if (transition.getEvent() == null) {
|
||||
continue;
|
||||
}
|
||||
String identifier = transition.getEvent().fullIdentifier() != null
|
||||
? transition.getEvent().fullIdentifier()
|
||||
: transition.getEvent().rawName();
|
||||
if (identifier == null || identifier.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String canonical = canonicalizeLabel(identifier, eventTypeFqn);
|
||||
if (classifyTriggerEvent(canonical) == TriggerEventKind.CANONICAL_ENUM
|
||||
&& !events.contains(canonical)) {
|
||||
events.add(canonical);
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
private static boolean hasConcretePolymorphicEvents(List<String> polymorphicEvents) {
|
||||
return polymorphicEvents != null
|
||||
&& polymorphicEvents.stream()
|
||||
.anyMatch(pe -> classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM);
|
||||
}
|
||||
|
||||
private static List<String> allPackageCanonicalEnumConstants(String eventTypeFqn, CodebaseContext context) {
|
||||
if (eventTypeFqn == null || eventTypeFqn.isBlank() || context == null) {
|
||||
return List.of();
|
||||
}
|
||||
String enumType = stripGenerics(eventTypeFqn);
|
||||
List<String> enumValues = context.getEnumValues(enumType);
|
||||
if (enumValues == null || enumValues.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> canonical = new ArrayList<>();
|
||||
for (String value : enumValues) {
|
||||
String constant = value.contains(".") ? value.substring(value.lastIndexOf('.') + 1) : value;
|
||||
canonical.add(enumType + "." + constant);
|
||||
}
|
||||
return canonical;
|
||||
}
|
||||
|
||||
public static String qualifyEventIdentifier(String event, String eventTypeFqn) {
|
||||
if (event == null || eventTypeFqn == null || event.isEmpty()) {
|
||||
return event;
|
||||
@@ -419,6 +505,12 @@ public final class MachineEnumCanonicalizer {
|
||||
return enumTypeFqn + "." + constant;
|
||||
}
|
||||
|
||||
if (typePart != null && !typePart.contains(".") && !enumTypesMatch(enumTypeFqn, typePart)
|
||||
&& importStyleEnumTypeMatches(typePart, enumTypeFqn)
|
||||
&& constant.matches("[A-Z_][A-Z0-9_]*")) {
|
||||
return enumTypeFqn + "." + constant;
|
||||
}
|
||||
|
||||
if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
|
||||
return enumTypeFqn + "." + stripped;
|
||||
}
|
||||
@@ -446,6 +538,11 @@ public final class MachineEnumCanonicalizer {
|
||||
return ref.substring(0, dot);
|
||||
}
|
||||
|
||||
private static boolean importStyleEnumTypeMatches(String typePart, String enumTypeFqn) {
|
||||
String simple = simpleName(enumTypeFqn);
|
||||
return simple.equals(typePart) || (simple + "s").equals(typePart);
|
||||
}
|
||||
|
||||
static boolean enumTypesMatch(String type1, String type2) {
|
||||
if (type1 == null || type2 == null) {
|
||||
return false;
|
||||
|
||||
@@ -12,6 +12,7 @@ import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
@@ -177,7 +178,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
TriggerPoint resolved = resolveTriggerPointParametersOriginal(tp, path, callGraph, finalParamNameRef);
|
||||
if (resolved == null) return null;
|
||||
String entryMethod = path.isEmpty() ? (tp.getClassName() + "." + tp.getMethodName()) : path.get(0);
|
||||
boolean isExternal = isExternalParameter(entryMethod, finalParamNameRef[0]);
|
||||
boolean isExternal = isExternalTrigger(entryMethod, finalParamNameRef[0], path, callGraph);
|
||||
|
||||
String pathConstraint = extractPathConstraints(path, callGraph);
|
||||
String triggerConstraint = resolved.getConstraint();
|
||||
@@ -1021,7 +1022,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
polymorphicEvents = narrowPolymorphicEventsWithPathBindings(polymorphicEvents, path, callGraph, tp);
|
||||
|
||||
Expression runtimeExpr = exprNode instanceof Expression expression ? expression : null;
|
||||
if (isRuntimeEnumParameter(runtimeExpr) || tp.isExternal()) {
|
||||
boolean hasConcretePolymorphic = polymorphicEvents.stream()
|
||||
.anyMatch(pe -> pe != null && !pe.startsWith("<SYMBOLIC:"));
|
||||
if (hasConcretePolymorphic
|
||||
&& (isRuntimeEnumParameter(runtimeExpr) || tp.isExternal()
|
||||
|| MachineEnumCanonicalizer.isDynamicTriggerExpression(resolvedValue))) {
|
||||
polymorphicEvents.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
||||
}
|
||||
|
||||
@@ -2730,10 +2735,28 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (!(exprNode instanceof MethodInvocation mi)) {
|
||||
return false;
|
||||
}
|
||||
if (!"valueOf".equals(mi.getName().getIdentifier()) || mi.arguments().size() != 1) {
|
||||
if (!"valueOf".equals(mi.getName().getIdentifier()) || mi.arguments().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return mi.arguments().get(0) instanceof SimpleName;
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
return !(arg instanceof StringLiteral);
|
||||
}
|
||||
|
||||
private boolean isExternalTrigger(
|
||||
String entryMethod,
|
||||
String finalParamName,
|
||||
List<String> path,
|
||||
Map<String, List<CallEdge>> callGraph) {
|
||||
if (isExternalParameter(entryMethod, finalParamName)) {
|
||||
return true;
|
||||
}
|
||||
Map<String, String> bindings = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
|
||||
for (String boundName : bindings.keySet()) {
|
||||
if (isExternalParameter(entryMethod, boundName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<String> resolveKeyedMapLookup(
|
||||
@@ -2860,7 +2883,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (hasConcrete) {
|
||||
narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
||||
}
|
||||
if (tp.isExternal()) {
|
||||
if (tp.isExternal() && hasConcrete) {
|
||||
narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
||||
}
|
||||
return narrowed;
|
||||
|
||||
@@ -2,6 +2,13 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.PropertyEnricher;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher;
|
||||
@@ -67,6 +74,52 @@ public class EnrichmentService {
|
||||
intelligence);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON re-export path: when source and entry points are available, rebuild call chains from the
|
||||
* call graph so dispatcher triggers regain {@code polymorphicEvents}/{@code external}, then relink.
|
||||
*/
|
||||
public void refreshCallChainsAndRelink(
|
||||
AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) {
|
||||
if (result.getMetadata() != null && context != null && intelligence != null) {
|
||||
List<CallChain> refreshed = CallChainEnricher.buildCallChainsForMachine(result, context, intelligence);
|
||||
if (refreshed != null) {
|
||||
List<CallChain> merged = mergeCallChainsByEntryPoint(result.getMetadata().getCallChains(), refreshed);
|
||||
result.setMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||
.triggers(result.getMetadata().getTriggers())
|
||||
.entryPoints(result.getMetadata().getEntryPoints())
|
||||
.callChains(merged)
|
||||
.properties(result.getMetadata().getProperties())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
relinkAfterPropertyResolution(result, context, intelligence);
|
||||
}
|
||||
|
||||
private static List<CallChain> mergeCallChainsByEntryPoint(
|
||||
List<CallChain> existing,
|
||||
List<CallChain> refreshed) {
|
||||
Map<String, CallChain> byEntryPoint = new LinkedHashMap<>();
|
||||
if (existing != null) {
|
||||
for (CallChain chain : existing) {
|
||||
byEntryPoint.putIfAbsent(entryPointKey(chain.getEntryPoint()), chain);
|
||||
}
|
||||
}
|
||||
for (CallChain chain : refreshed) {
|
||||
byEntryPoint.put(entryPointKey(chain.getEntryPoint()), chain);
|
||||
}
|
||||
return new ArrayList<>(byEntryPoint.values());
|
||||
}
|
||||
|
||||
private static String entryPointKey(EntryPoint entryPoint) {
|
||||
if (entryPoint == null) {
|
||||
return "";
|
||||
}
|
||||
if (entryPoint.getName() != null && !entryPoint.getName().isBlank()) {
|
||||
return entryPoint.getName();
|
||||
}
|
||||
return entryPoint.getClassName() + "#" + entryPoint.getMethodName();
|
||||
}
|
||||
|
||||
private static void runEnrichers(
|
||||
List<AnalysisEnricher> enrichers,
|
||||
AnalysisResult result,
|
||||
|
||||
@@ -194,7 +194,15 @@ public class ExportService {
|
||||
result.getEventTypeFqn(),
|
||||
context);
|
||||
AnalysisResultFinalizer.applyCanonicalization(result, context, machineTypes);
|
||||
enrichmentService.relinkAfterPropertyResolution(result, context, null);
|
||||
if (context != null && sourceRoot != null
|
||||
&& result.getMetadata() != null
|
||||
&& result.getMetadata().getEntryPoints() != null
|
||||
&& !result.getMetadata().getEntryPoints().isEmpty()) {
|
||||
JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, sourceRoot);
|
||||
enrichmentService.refreshCallChainsAndRelink(result, context, intelligence);
|
||||
} else {
|
||||
enrichmentService.relinkAfterPropertyResolution(result, context, null);
|
||||
}
|
||||
AnalysisResultFinalizer.finalizeResult(result, context, machineTypes);
|
||||
|
||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||
|
||||
Reference in New Issue
Block a user