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:
@@ -45,6 +45,7 @@ public class CodebaseContext {
|
||||
private Path projectRoot;
|
||||
private final Map<String, Object> cache = new HashMap<>();
|
||||
private AccessorIndex accessorIndex = AccessorIndex.empty();
|
||||
private boolean inlineAccessors = true;
|
||||
|
||||
public Map<String, Object> getCache() {
|
||||
return cache;
|
||||
@@ -54,6 +55,14 @@ public class CodebaseContext {
|
||||
return accessorIndex;
|
||||
}
|
||||
|
||||
public boolean isInlineAccessors() {
|
||||
return inlineAccessors;
|
||||
}
|
||||
|
||||
public void setInlineAccessors(boolean inlineAccessors) {
|
||||
this.inlineAccessors = inlineAccessors;
|
||||
}
|
||||
|
||||
public void setProjectRoot(Path root) {
|
||||
this.projectRoot = root;
|
||||
}
|
||||
@@ -247,7 +256,15 @@ public class CodebaseContext {
|
||||
}
|
||||
}
|
||||
|
||||
this.accessorIndex = new AccessorIndexBuilder().build(this);
|
||||
this.accessorIndex = inlineAccessors
|
||||
? new AccessorIndexBuilder().build(this)
|
||||
: AccessorIndex.empty();
|
||||
if (inlineAccessors) {
|
||||
log.info("Built accessor index: {} trivial accessors across {} types",
|
||||
accessorIndex.trivialCount(), accessorIndex.typeCount());
|
||||
} else {
|
||||
log.info("Accessor inlining disabled; skipping accessor index build.");
|
||||
}
|
||||
}
|
||||
|
||||
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {
|
||||
|
||||
@@ -60,6 +60,9 @@ public class ExporterCommand implements Callable<Integer> {
|
||||
@Option(names = {"--include-generated-sources"}, description = "Scan existing Maven target/ and Gradle build/ trees for pre-generated .java sources (MapStruct, annotation processors, etc.). Does not run Maven or Gradle.", negatable = true, defaultValue = "true", fallbackValue = "true")
|
||||
private boolean includeGeneratedSources = true;
|
||||
|
||||
@Option(names = {"--inline-accessors"}, description = "Index trivial JavaBean/record accessors at scan time and inline them during call-chain and constant resolution.", negatable = true, defaultValue = "true", fallbackValue = "true")
|
||||
private boolean inlineAccessors = true;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
var out = spec.commandLine().getOut();
|
||||
@@ -90,7 +93,7 @@ public class ExporterCommand implements Callable<Integer> {
|
||||
if (jsonFile != null) {
|
||||
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
|
||||
} else {
|
||||
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, includeGeneratedSources);
|
||||
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, includeGeneratedSources, inlineAccessors);
|
||||
}
|
||||
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
||||
return 0;
|
||||
|
||||
@@ -75,8 +75,13 @@ public class ExportService {
|
||||
}
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean includeGeneratedSources) throws IOException {
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, eventFormat, stateFormat, includeGeneratedSources, true);
|
||||
}
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean includeGeneratedSources, boolean inlineAccessors) throws IOException {
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setActiveProfiles(activeProfiles);
|
||||
context.setInlineAccessors(inlineAccessors);
|
||||
|
||||
click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver siblingResolver =
|
||||
new click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver();
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user