From 8c4f15133effd2a1a4a8131fd3d329746b894580 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 06:24:40 +0200 Subject: [PATCH] Defer trigger event qualification and resolve types from bindings Stop rewriting trigger events in TriggerPoint construction; qualify with context during property resolution and export. Upgrade heuristic simple names to JDT binding FQNs for user types only, preserving java.* platform types. Co-authored-by: Cursor --- .../analysis/model/AnalysisResult.java | 33 ++++++++---- .../analysis/model/TriggerPoint.java | 4 +- .../resolver/MachineEnumCanonicalizer.java | 17 ++++-- .../analysis/service/TypeResolver.java | 20 ++++++- .../service/ExportService.java | 10 ++-- ...hineEnumCanonicalizerCrossPackageTest.java | 18 +++++++ .../service/AnalysisResultFinalizerTest.java | 5 +- .../analysis/service/TypeResolverTest.java | 54 +++++++++++++++++++ 8 files changed, 140 insertions(+), 21 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java index d8396ac..cd72887 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/AnalysisResult.java @@ -9,6 +9,7 @@ import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition; 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 lombok.Builder; import lombok.Getter; @@ -46,6 +47,10 @@ public class AnalysisResult { private CodebaseMetadata metadata = CodebaseMetadata.empty(); public void applyResolution(Map properties) { + applyResolution(properties, null); + } + + public void applyResolution(Map properties, CodebaseContext context) { var resolver = new click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver(); // 1. Resolve start/end states strings @@ -89,12 +94,12 @@ public class AnalysisResult { if (metadata != null) { List resolvedTriggers = metadata.getTriggers() == null ? null : metadata.getTriggers().stream() - .map(trigger -> resolveTrigger(trigger, resolver, properties)) + .map(trigger -> resolveTrigger(trigger, resolver, properties, context)) .collect(java.util.stream.Collectors.toList()); List resolvedCallChains = metadata.getCallChains() == null ? null : metadata.getCallChains().stream() - .map(chain -> resolveCallChain(chain, resolver, properties)) + .map(chain -> resolveCallChain(chain, resolver, properties, context)) .collect(java.util.stream.Collectors.toList()); if (metadata.getEntryPoints() != null) { @@ -136,17 +141,26 @@ public class AnalysisResult { private static TriggerPoint resolveTrigger( TriggerPoint trigger, click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver, - Map properties) { + Map properties, + CodebaseContext context) { List polymorphicEvents = trigger.getPolymorphicEvents() == null ? null : trigger.getPolymorphicEvents().stream() .map(value -> resolver.resolveValue(value, properties)) .map(value -> MachineEnumCanonicalizer.qualifyEventIdentifier( - value, trigger.getEventTypeFqn())) + value, trigger.getEventTypeFqn(), context)) .collect(java.util.stream.Collectors.toList()); + String resolvedEvent = trigger.getEvent() != null ? resolver.resolveValue(trigger.getEvent(), properties) : null; + String resolvedSource = trigger.getSourceState() != null + ? resolver.resolveValue(trigger.getSourceState(), properties) + : null; return trigger.toBuilder() - .event(trigger.getEvent() != null ? resolver.resolveValue(trigger.getEvent(), properties) : null) - .sourceState(trigger.getSourceState() != null - ? resolver.resolveValue(trigger.getSourceState(), properties) + .event(resolvedEvent != null + ? MachineEnumCanonicalizer.qualifyEventIdentifier( + resolvedEvent, trigger.getEventTypeFqn(), context) + : null) + .sourceState(resolvedSource != null + ? MachineEnumCanonicalizer.canonicalizeLabel( + resolvedSource, trigger.getStateTypeFqn(), context) : null) .polymorphicEvents(polymorphicEvents) .build(); @@ -155,10 +169,11 @@ public class AnalysisResult { private static CallChain resolveCallChain( CallChain chain, click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver, - Map properties) { + Map properties, + CodebaseContext context) { TriggerPoint trigger = chain.getTriggerPoint() == null ? null - : resolveTrigger(chain.getTriggerPoint(), resolver, properties); + : resolveTrigger(chain.getTriggerPoint(), resolver, properties, context); List matchedTransitions = chain.getMatchedTransitions() == null ? null : chain.getMatchedTransitions().stream() .map(matched -> MatchedTransition.builder() diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java index 8718e25..0033c90 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java @@ -50,11 +50,11 @@ public class TriggerPoint { this.sourceFile = sourceFile; this.sourceModule = sourceModule; this.stateMachineId = stateMachineId; - this.sourceState = MachineEnumCanonicalizer.canonicalizeLabel(sourceState, stateTypeFqn); + this.sourceState = sourceState; this.lineNumber = lineNumber; this.stateTypeFqn = stateTypeFqn; this.eventTypeFqn = eventTypeFqn; - this.event = MachineEnumCanonicalizer.qualifyEventIdentifier(event, eventTypeFqn); + this.event = event; this.polymorphicEvents = polymorphicEvents; this.external = external; this.constraint = constraint; 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 6190c88..54060c4 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 @@ -533,6 +533,10 @@ public final class MachineEnumCanonicalizer { } public static String qualifyEventIdentifier(String event, String eventTypeFqn) { + return qualifyEventIdentifier(event, eventTypeFqn, null); + } + + public static String qualifyEventIdentifier(String event, String eventTypeFqn, CodebaseContext context) { if (event == null || eventTypeFqn == null || event.isEmpty()) { return event; } @@ -554,8 +558,8 @@ public final class MachineEnumCanonicalizer { if (isStringOrPrimitiveType(eventTypeFqn)) { return event; } - if (isMachineEnumReference(event, eventTypeFqn)) { - return canonicalizeLabel(event, stripGenerics(eventTypeFqn)); + if (isMachineEnumReference(event, eventTypeFqn, context)) { + return canonicalizeLabel(event, stripGenerics(eventTypeFqn), context); } return event; } @@ -742,6 +746,10 @@ public final class MachineEnumCanonicalizer { } public static boolean isMachineEnumReference(String value, String enumTypeFqn) { + return isMachineEnumReference(value, enumTypeFqn, null); + } + + public static boolean isMachineEnumReference(String value, String enumTypeFqn, CodebaseContext context) { if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) { return false; } @@ -765,7 +773,10 @@ public final class MachineEnumCanonicalizer { if (typePart == null) { return true; } - return enumTypesMatch(enumTypeFqn, typePart); + if (context != null && context.isAmbiguousSimpleName(typePart)) { + return false; + } + return enumTypesMatch(enumTypeFqn, typePart, context); } public static String canonicalizeLabel(String value, String enumTypeFqn) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java index c6c3522..f040194 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java @@ -79,7 +79,25 @@ public class TypeResolver { } } - return simpleName; + return preferBindingFqn(type, simpleName); + } + + private String preferBindingFqn(Type type, String heuristic) { + if (!context.isResolveBindings() || heuristic == null || heuristic.contains(".")) { + return heuristic; + } + ITypeBinding binding = type.resolveBinding(); + if (binding == null || binding.isRecovered()) { + return heuristic; + } + String bindingFqn = binding.getErasure().getQualifiedName(); + if (bindingFqn == null || bindingFqn.isBlank() || !bindingFqn.contains(".") || bindingFqn.startsWith("<")) { + return heuristic; + } + if (bindingFqn.startsWith("java.") || bindingFqn.startsWith("javax.") || bindingFqn.startsWith("jakarta.")) { + return heuristic; + } + return bindingFqn; } public int getParameterIndex(String methodFqn, String paramName) { 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 d22051a..c6fafb4 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 @@ -167,7 +167,7 @@ public class ExportService { JsonImportService jsonImportService = new JsonImportService(); AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile); - resolveProperties(result, activeProfiles); + resolveProperties(result, activeProfiles, null); CodebaseContext context = null; Path sourceRoot = optionalSourceDir; @@ -215,7 +215,7 @@ public class ExportService { generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat); } - private void resolveProperties(AnalysisResult result, List activeProfiles) { + private void resolveProperties(AnalysisResult result, List activeProfiles, CodebaseContext context) { Map> allProps = result.getMetadata().getProperties(); if (allProps == null || allProps.isEmpty()) return; @@ -233,7 +233,7 @@ public class ExportService { } // 2. Delegate to result for resolution - result.applyResolution(merged); + result.applyResolution(merged, context); } private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List selectedFormats, boolean renderChoicesAsDiamonds, List flows, String machineFilter, List activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException { @@ -305,7 +305,7 @@ public class ExportService { .build(); enrichmentService.enrichPreProperty(result, context, intelligence); - resolveProperties(result, activeProfiles); + resolveProperties(result, activeProfiles, context); enrichmentService.enrichPostProperty(result, context, intelligence); AnalysisResultFinalizer.finalizeResult(result, context); @@ -347,7 +347,7 @@ public class ExportService { .build(); enrichmentService.enrichPreProperty(result, context, intelligence); - resolveProperties(result, activeProfiles); + resolveProperties(result, activeProfiles, context); enrichmentService.enrichPostProperty(result, context, intelligence); AnalysisResultFinalizer.finalizeResult(result, context); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerCrossPackageTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerCrossPackageTest.java index 828afd2..621f792 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerCrossPackageTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerCrossPackageTest.java @@ -92,4 +92,22 @@ class MachineEnumCanonicalizerCrossPackageTest { "com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent.SHIP"); } + + @Test + void shouldNotQualifyAmbiguousImportStyleEventReference(@TempDir Path tempDir) throws Exception { + Files.createDirectories(tempDir.resolve("a")); + Files.createDirectories(tempDir.resolve("b")); + Files.writeString(tempDir.resolve("a/OrderEvent.java"), + "package a; public enum OrderEvent { PAY }"); + Files.writeString(tempDir.resolve("b/OrderEvent.java"), + "package b; public enum OrderEvent { CANCEL }"); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + assertThat(MachineEnumCanonicalizer.qualifyEventIdentifier( + "OrderEvent.PAY", "a.OrderEvent", context)).isEqualTo("OrderEvent.PAY"); + assertThat(MachineEnumCanonicalizer.isMachineEnumReference( + "OrderEvent.PAY", "a.OrderEvent", context)).isFalse(); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AnalysisResultFinalizerTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AnalysisResultFinalizerTest.java index 8baf7cf..f2f5fc3 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AnalysisResultFinalizerTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AnalysisResultFinalizerTest.java @@ -110,9 +110,12 @@ class AnalysisResultFinalizerTest { .metadata(CodebaseMetadata.builder().triggers(List.of(trigger)).build()) .build(); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + result.applyResolution(Map.of( "app.event", "OrderEvent.PAY", - "app.state", "OrderState.NEW")); + "app.state", "OrderState.NEW"), context); TriggerPoint resolved = result.getMetadata().getTriggers().get(0); assertThat(resolved.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY"); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolverTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolverTest.java index b936564..8fdc888 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolverTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolverTest.java @@ -1,6 +1,10 @@ package click.kamil.springstatemachineexporter.analysis.service; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -48,4 +52,54 @@ class TypeResolverTest { assertThat(resolver.isTypeCompatible("a.OrderEvent", "b.OrderEvent")).isFalse(); assertThat(resolver.isTypeCompatible("a.OrderEvent", "a.OrderEvent")).isTrue(); } + + @Test + void shouldUpgradeSimpleHeuristicTypeToBindingFqnForUserTypes(@TempDir Path tempDir) throws Exception { + Files.createDirectories(tempDir.resolve("com/example/order")); + Files.writeString(tempDir.resolve("com/example/order/OrderEvent.java"), """ + package com.example.order; + public enum OrderEvent { PAY } + """); + Files.writeString(tempDir.resolve("App.java"), """ + package com.example; + import com.example.order.OrderEvent; + class StateMachine { + void sendEvent(OrderEvent e) {} + } + """); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.scan(tempDir); + TypeResolver resolver = new TypeResolver(context); + + TypeDeclaration td = context.getTypeDeclaration("com.example.StateMachine"); + MethodDeclaration md = context.findMethodDeclaration(td, "sendEvent", true); + SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(0); + + assertThat(resolver.resolveTypeToFqn(param.getType(), (CompilationUnit) td.getRoot())) + .isEqualTo("com.example.order.OrderEvent"); + } + + @Test + void shouldKeepHeuristicStringTypeWhenBindingsWouldReturnJavaLangString(@TempDir Path tempDir) throws Exception { + Files.writeString(tempDir.resolve("App.java"), """ + package com.example; + class Api { + void handle(String value) {} + } + """); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.scan(tempDir); + TypeResolver resolver = new TypeResolver(context); + + TypeDeclaration td = context.getTypeDeclaration("com.example.Api"); + MethodDeclaration md = context.findMethodDeclaration(td, "handle", true); + SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(0); + + assertThat(resolver.resolveTypeToFqn(param.getType(), (CompilationUnit) td.getRoot())) + .isEqualTo("String"); + } }