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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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<String, String> properties) {
|
||||
applyResolution(properties, null);
|
||||
}
|
||||
|
||||
public void applyResolution(Map<String, String> 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<TriggerPoint> 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<CallChain> 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<String, String> properties) {
|
||||
Map<String, String> properties,
|
||||
CodebaseContext context) {
|
||||
List<String> 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());
|
||||
return trigger.toBuilder()
|
||||
.event(trigger.getEvent() != null ? resolver.resolveValue(trigger.getEvent(), properties) : null)
|
||||
.sourceState(trigger.getSourceState() != null
|
||||
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(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<String, String> properties) {
|
||||
Map<String, String> properties,
|
||||
CodebaseContext context) {
|
||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
||||
? null
|
||||
: resolveTrigger(chain.getTriggerPoint(), resolver, properties);
|
||||
: resolveTrigger(chain.getTriggerPoint(), resolver, properties, context);
|
||||
List<MatchedTransition> matchedTransitions = chain.getMatchedTransitions() == null ? null
|
||||
: chain.getMatchedTransitions().stream()
|
||||
.map(matched -> MatchedTransition.builder()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<String> activeProfiles) {
|
||||
private void resolveProperties(AnalysisResult result, List<String> activeProfiles, CodebaseContext context) {
|
||||
Map<String, Map<String, String>> 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<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter, List<String> 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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user