Add --inline-accessors CLI flag, layered dispatcher regression goldens, and accessor index docs.

Expose accessor index toggling through ExportService and picocli, lock in layered_dispatcher_sample exports as golden fixtures, and document the static analysis pipeline in the README.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 20:29:07 +02:00
parent a108d8cc6e
commit 8fa6a44e75
14 changed files with 2430 additions and 2 deletions

View File

@@ -155,6 +155,18 @@ public class RegressionTest {
root.resolve("state_machines/polymorphic_events_sample"),
Path.of("src/test/resources/golden/PolymorphicStateMachineConfiguration"),
"PolymorphicStateMachineConfiguration"
),
new TestScenario(
"Layered Dispatcher (Order)",
root.resolve("state_machines/layered_dispatcher_sample"),
Path.of("src/test/resources/golden/StandardOrderStateMachineConfiguration"),
"StandardOrderStateMachineConfiguration"
),
new TestScenario(
"Layered Dispatcher (Document)",
root.resolve("state_machines/layered_dispatcher_sample"),
Path.of("src/test/resources/golden/StandardDocumentStateMachineConfiguration"),
"StandardDocumentStateMachineConfiguration"
)
);
}

View File

@@ -0,0 +1,106 @@
package click.kamil.springstatemachineexporter.service;
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ExportServiceInlineAccessorsTest {
@Test
void shouldSkipAccessorIndexWhenInlineAccessorsDisabled(@TempDir Path tempDir) throws IOException {
Path sourceRoot = tempDir.resolve("project");
writeJava(sourceRoot, "com/example/Payload.java", """
package com.example;
public class Payload {
private String event = "PAY";
public String getEvent() { return event; }
}
""");
writeJava(sourceRoot, "com/example/Config.java", """
package com.example;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
@Configuration
@EnableStateMachine
public class Config extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states.withStates().initial("NEW").state("PAID");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("NEW").target("PAID").event("PAY");
}
}
""");
CodebaseContext enabledContext = scanContext(sourceRoot, true);
assertThat(enabledContext.getAccessorIndex().trivialCount()).isGreaterThan(0);
CodebaseContext disabledContext = scanContext(sourceRoot, false);
assertThat(disabledContext.getAccessorIndex().trivialCount()).isZero();
}
@Test
void shouldExportWithInlineAccessorsFlag(@TempDir Path inputDir, @TempDir Path outputDir) throws IOException {
writeJava(inputDir, "com/example/SimpleConfig.java", """
package com.example;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
@Configuration
@EnableStateMachine
public class SimpleConfig extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states.withStates().initial("A").state("B");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("A").target("B").event("GO");
}
}
""");
ExportService exportService = new ExportService(List.of(new JsonExporter()));
exportService.runExporter(inputDir, outputDir, List.of("json"), true, List.of(), null, null,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
true,
false);
try (var stream = Files.list(outputDir)) {
assertThat(stream.filter(Files::isDirectory).count()).isGreaterThan(0);
}
}
private static CodebaseContext scanContext(Path sourceRoot, boolean inlineAccessors) throws IOException {
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(sourceRoot.toAbsolutePath().toString()));
context.setInlineAccessors(inlineAccessors);
context.scan(sourceRoot);
return context;
}
private static void writeJava(Path root, String relativePath, String source) throws IOException {
Path file = root.resolve(relativePath);
Files.createDirectories(file.getParent());
Files.writeString(file, source);
}
}

View File

@@ -0,0 +1,14 @@
digraph statemachine {
rankdir=LR;
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
edge [fontname="Arial", fontsize=10];
_start [shape=circle, label="", fillcolor=black, width=0.1];
_start -> DRAFT;
REJECTED [fillcolor=lightgray];
APPROVED [fillcolor=lightgray];
DRAFT -> SUBMITTED [label="DocumentTransitionEvent.SUBMIT", style="solid", color="black"];
SUBMITTED -> APPROVED [label="DocumentTransitionEvent.APPROVE", style="solid", color="black"];
SUBMITTED -> REJECTED [label="DocumentTransitionEvent.REJECT", style="solid", color="black"];
}

View File

@@ -0,0 +1,34 @@
@startuml
!pragma layout smetana
set separator none
hide empty description
hide stereotype
skinparam state {
BackgroundColor white
BorderColor #94a3b8
BorderThickness 1
FontName Inter
FontSize 9
FontStyle bold
RoundCorner 20
Padding 1
}
skinparam shadowing false
skinparam ArrowFontName JetBrains Mono
skinparam ArrowFontSize 8
skinparam ArrowColor #cbd5e1
skinparam ArrowThickness 1
skinparam dpi 110
skinparam svgLinkTarget _self
[*] --> DocumentState.DRAFT
DocumentState.DRAFT -[#1E90FF,bold]-> DocumentState.SUBMITTED <<external>> : DocumentTransitionEvent.SUBMIT
DocumentState.SUBMITTED -[#1E90FF,bold]-> DocumentState.APPROVED <<external>> : DocumentTransitionEvent.APPROVE
DocumentState.SUBMITTED -[#1E90FF,bold]-> DocumentState.REJECTED <<external>> : DocumentTransitionEvent.REJECT
DocumentState.REJECTED --> [*]
DocumentState.APPROVED --> [*]
@enduml

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="DRAFT">
<state id="DRAFT">
<transition target="SUBMITTED" event="DocumentTransitionEvent.SUBMIT"/>
</state>
<state id="SUBMITTED">
<transition target="APPROVED" event="DocumentTransitionEvent.APPROVE"/>
<transition target="REJECTED" event="DocumentTransitionEvent.REJECT"/>
</state>
<state id="APPROVED">
</state>
<state id="REJECTED">
</state>
</scxml>

View File

@@ -0,0 +1,14 @@
digraph statemachine {
rankdir=LR;
node [shape=rounded, style=filled, fillcolor=white, fontname="Arial"];
edge [fontname="Arial", fontsize=10];
_start [shape=circle, label="", fillcolor=black, width=0.1];
_start -> NEW;
SHIPPED [fillcolor=lightgray];
CANCELLED [fillcolor=lightgray];
NEW -> PAID [label="OrderTransitionEvent.PAY", style="solid", color="black"];
PAID -> SHIPPED [label="OrderTransitionEvent.SHIP", style="solid", color="black"];
PAID -> CANCELLED [label="OrderTransitionEvent.CANCEL", style="solid", color="black"];
}

View File

@@ -0,0 +1,34 @@
@startuml
!pragma layout smetana
set separator none
hide empty description
hide stereotype
skinparam state {
BackgroundColor white
BorderColor #94a3b8
BorderThickness 1
FontName Inter
FontSize 9
FontStyle bold
RoundCorner 20
Padding 1
}
skinparam shadowing false
skinparam ArrowFontName JetBrains Mono
skinparam ArrowFontSize 8
skinparam ArrowColor #cbd5e1
skinparam ArrowThickness 1
skinparam dpi 110
skinparam svgLinkTarget _self
[*] --> OrderState.NEW
OrderState.NEW -[#1E90FF,bold]-> OrderState.PAID <<external>> : OrderTransitionEvent.PAY
OrderState.PAID -[#1E90FF,bold]-> OrderState.SHIPPED <<external>> : OrderTransitionEvent.SHIP
OrderState.PAID -[#1E90FF,bold]-> OrderState.CANCELLED <<external>> : OrderTransitionEvent.CANCEL
OrderState.SHIPPED --> [*]
OrderState.CANCELLED --> [*]
@enduml

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="NEW">
<state id="NEW">
<transition target="PAID" event="OrderTransitionEvent.PAY"/>
</state>
<state id="PAID">
<transition target="SHIPPED" event="OrderTransitionEvent.SHIP"/>
<transition target="CANCELLED" event="OrderTransitionEvent.CANCEL"/>
</state>
<state id="SHIPPED">
</state>
<state id="CANCELLED">
</state>
</scxml>