Fix identifier consistency on JSON re-export and after property resolution.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
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.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -117,6 +119,37 @@ public class AnalysisResult {
|
|||||||
.properties(metadata.getProperties())
|
.properties(metadata.getProperties())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canonicalizeResolvedIdentifiers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void canonicalizeResolvedIdentifiers() {
|
||||||
|
if (stateTypeFqn == null && eventTypeFqn == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||||
|
new StateMachineTypeResolver.MachineTypes(stateTypeFqn, eventTypeFqn);
|
||||||
|
MachineEnumCanonicalizer.canonicalizeTransitions(transitions, machineTypes);
|
||||||
|
if (states != null) {
|
||||||
|
this.states = MachineEnumCanonicalizer.canonicalizeStates(states, stateTypeFqn);
|
||||||
|
}
|
||||||
|
if (startStates != null) {
|
||||||
|
startStates = MachineEnumCanonicalizer.canonicalizeStateLabels(startStates, stateTypeFqn);
|
||||||
|
}
|
||||||
|
if (endStates != null) {
|
||||||
|
endStates = MachineEnumCanonicalizer.canonicalizeStateLabels(endStates, stateTypeFqn);
|
||||||
|
}
|
||||||
|
if (metadata != null && metadata.getTriggers() != null) {
|
||||||
|
List<TriggerPoint> canonicalTriggers = metadata.getTriggers().stream()
|
||||||
|
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
metadata = CodebaseMetadata.builder()
|
||||||
|
.triggers(canonicalTriggers)
|
||||||
|
.entryPoints(metadata.getEntryPoints())
|
||||||
|
.callChains(metadata.getCallChains())
|
||||||
|
.properties(metadata.getProperties())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TriggerPoint resolveTrigger(
|
private static TriggerPoint resolveTrigger(
|
||||||
|
|||||||
@@ -59,7 +59,23 @@ public final class AnalysisResultFinalizer {
|
|||||||
result.setEventTypeFqn(machineTypes.eventTypeFqn());
|
result.setEventTypeFqn(machineTypes.eventTypeFqn());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applyCanonicalization(
|
/**
|
||||||
|
* Canonicalizes transitions, states, triggers, and matched transitions without validation.
|
||||||
|
* Safe to call before transition relinking when machine types are known.
|
||||||
|
*/
|
||||||
|
public static void applyCanonicalization(
|
||||||
|
AnalysisResult result,
|
||||||
|
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||||
|
if (result == null || machineTypes == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (machineTypes.stateTypeFqn() == null && machineTypes.eventTypeFqn() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyCanonicalizationFields(result, machineTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyCanonicalizationFields(
|
||||||
AnalysisResult result,
|
AnalysisResult result,
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes) {
|
StateMachineTypeResolver.MachineTypes machineTypes) {
|
||||||
MachineEnumCanonicalizer.canonicalizeTransitions(result.getTransitions(), machineTypes);
|
MachineEnumCanonicalizer.canonicalizeTransitions(result.getTransitions(), machineTypes);
|
||||||
|
|||||||
@@ -187,14 +187,14 @@ public class ExportService {
|
|||||||
log.info("JSON re-export: no source project found; using embedded machine types if present");
|
log.info("JSON re-export: no source project found; using embedded machine types if present");
|
||||||
}
|
}
|
||||||
|
|
||||||
enrichmentService.relinkAfterPropertyResolution(result, context, null);
|
|
||||||
|
|
||||||
StateMachineTypeResolver.MachineTypes machineTypes =
|
StateMachineTypeResolver.MachineTypes machineTypes =
|
||||||
click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory.resolveMachineTypes(
|
click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory.resolveMachineTypes(
|
||||||
result.getName(),
|
result.getName(),
|
||||||
result.getStateTypeFqn(),
|
result.getStateTypeFqn(),
|
||||||
result.getEventTypeFqn(),
|
result.getEventTypeFqn(),
|
||||||
context);
|
context);
|
||||||
|
AnalysisResultFinalizer.applyCanonicalization(result, machineTypes);
|
||||||
|
enrichmentService.relinkAfterPropertyResolution(result, context, null);
|
||||||
AnalysisResultFinalizer.finalizeResult(result, context, machineTypes);
|
AnalysisResultFinalizer.finalizeResult(result, context, machineTypes);
|
||||||
|
|
||||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
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.CodebaseMetadata;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
||||||
@@ -16,6 +17,7 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -95,6 +97,75 @@ class JsonRoundTripCanonicalizationTest {
|
|||||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRelinkMatchedTransitionsAfterPreCanonicalizingShortFormJson(@TempDir Path tempDir) throws Exception {
|
||||||
|
writeSampleProject(tempDir);
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.event("OrderEvent.PAY")
|
||||||
|
.sourceState("OrderState.NEW")
|
||||||
|
.className("com.example.web.OrderController")
|
||||||
|
.methodName("pay")
|
||||||
|
.sourceFile("OrderController.java")
|
||||||
|
.build();
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(trigger)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult shortForm = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfiguration")
|
||||||
|
.stateTypeFqn("com.example.order.OrderState")
|
||||||
|
.eventTypeFqn("com.example.order.OrderEvent")
|
||||||
|
.transitions(List.of(shortTransition(
|
||||||
|
"OrderEvent.PAY",
|
||||||
|
"OrderState.NEW",
|
||||||
|
"OrderState.PAID")))
|
||||||
|
.metadata(CodebaseMetadata.builder()
|
||||||
|
.triggers(List.of(trigger))
|
||||||
|
.callChains(List.of(chain))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Path jsonFile = tempDir.resolve("machine.json");
|
||||||
|
Files.writeString(jsonFile, new JsonExporter().export(shortForm, ExportOptions.builder().build()));
|
||||||
|
|
||||||
|
Path outputDir = tempDir.resolve("out");
|
||||||
|
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||||
|
exportService.runJsonExporter(jsonFile, outputDir, List.of("json"));
|
||||||
|
|
||||||
|
AnalysisResult roundTripped = new JsonImportService().importAnalysisResult(
|
||||||
|
outputDir.resolve("com.example.config.OrderStateMachineConfiguration")
|
||||||
|
.resolve("com.example.config.OrderStateMachineConfiguration.json"));
|
||||||
|
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains()).hasSize(1);
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
|
||||||
|
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getSourceState())
|
||||||
|
.isEqualTo("com.example.order.OrderState.NEW");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDedupeStatesByFullIdentifierAfterPropertyResolution(@TempDir Path tempDir) throws Exception {
|
||||||
|
writeSampleProject(tempDir);
|
||||||
|
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfiguration")
|
||||||
|
.stateTypeFqn("com.example.order.OrderState")
|
||||||
|
.eventTypeFqn("com.example.order.OrderEvent")
|
||||||
|
.states(Set.of(
|
||||||
|
State.of("OrderState.NEW", "OrderState.NEW"),
|
||||||
|
State.of("UNDEFINED", "com.example.order.OrderState.NEW")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
result.applyResolution(Map.of());
|
||||||
|
|
||||||
|
assertThat(result.getStates()).hasSize(1);
|
||||||
|
State only = result.getStates().iterator().next();
|
||||||
|
assertThat(only.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
|
||||||
|
assertThat(only.rawName()).isEqualTo("OrderState.NEW");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindProjectRootFromJsonDirectory(@TempDir Path tempDir) throws Exception {
|
void shouldFindProjectRootFromJsonDirectory(@TempDir Path tempDir) throws Exception {
|
||||||
Path projectRoot = tempDir.resolve("my-app");
|
Path projectRoot = tempDir.resolve("my-app");
|
||||||
|
|||||||
Reference in New Issue
Block a user