Index trivial accessors and inline them in dataflow, constant extraction, and call graph resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 18:17:17 +02:00
parent 0628710106
commit 7f12ed3f3a
19 changed files with 2591 additions and 7 deletions

View File

@@ -1,5 +1,7 @@
package click.kamil.springstatemachineexporter;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
import click.kamil.springstatemachineexporter.exporter.Dot;
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
@@ -21,8 +23,12 @@ import static org.assertj.core.api.Assertions.assertThat;
public class RegressionTest {
private final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
private final ExportService exportService = new ExportService(exporters);
private static final List<StateMachineExporter> EXPORTERS =
List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
private ExportService exportService() {
return new ExportService(EXPORTERS);
}
private static Path findProjectRoot() {
Path current = Path.of(".").toAbsolutePath();
@@ -155,9 +161,10 @@ public class RegressionTest {
@ParameterizedTest(name = "{0}")
@MethodSource("provideTestScenarios")
@Execution(ExecutionMode.CONCURRENT)
void testOutputMatchesGolden(TestScenario scenario, @TempDir Path tempDir) throws Exception {
System.out.println("Running test for " + scenario.name());
if (exportService == null) System.out.println("exportService is NULL");
ExportService exportService = exportService();
if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
if (tempDir == null) System.out.println("tempDir is NULL");

View File

@@ -0,0 +1,263 @@
package click.kamil.springstatemachineexporter.analysis.index;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Nested;
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.Optional;
import static org.assertj.core.api.Assertions.assertThat;
class AccessorIndexBuilderTest {
@Nested
class ClassAccessors {
@Test
void shouldIndexTrivialGetterAndSetter(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Order.java", """
package com.example;
public class Order {
private String event;
public String getEvent() { return event; }
public void setEvent(String event) { this.event = event; }
}
""");
AccessorIndex index = scan(tempDir);
assertThat(index.lookup("com.example.Order", "getEvent")).isPresent();
assertThat(index.lookup("com.example.Order", "setEvent")).isPresent();
assertThat(index.accessorsForType("com.example.Order")).hasSize(2);
}
@Test
void shouldIndexNestedClassAccessors(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Outer.java", """
package com.example;
public class Outer {
static class Inner {
private String code;
public String getCode() { return code; }
}
}
""");
AccessorIndex index = scan(tempDir);
assertThat(index.lookup("com.example.Outer.Inner", "getCode")).isPresent();
}
}
@Nested
class Inheritance {
@Test
void shouldInheritParentTrivialGetter(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Base.java", """
package com.example;
public class Base {
protected String event;
public String getEvent() { return event; }
}
""");
writeJava(tempDir, "com/example/Child.java", """
package com.example;
public class Child extends Base {
public void run() {}
}
""");
AccessorIndex index = scan(tempDir);
assertThat(index.lookup("com.example.Child", "getEvent")).isPresent();
assertThat(index.lookup("com.example.Child", "getEvent").orElseThrow().ownerFqn())
.isEqualTo("com.example.Child");
}
@Test
void shouldNotInheritWhenChildOverridesWithLogic(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Base.java", """
package com.example;
public class Base {
protected String event;
public String getEvent() { return event; }
}
""");
writeJava(tempDir, "com/example/Child.java", """
package com.example;
public class Child extends Base {
@Override
public String getEvent() {
return event == null ? "NONE" : event;
}
}
""");
AccessorIndex index = scan(tempDir);
assertThat(index.lookup("com.example.Base", "getEvent")).isPresent();
assertThat(index.lookup("com.example.Child", "getEvent")).isEmpty();
}
@Test
void shouldPreferChildTrivialOverride(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Base.java", """
package com.example;
public class Base {
protected String event;
public String getEvent() { return event; }
}
""");
writeJava(tempDir, "com/example/Child.java", """
package com.example;
public class Child extends Base {
@Override
public String getEvent() { return this.event; }
}
""");
AccessorIndex index = scan(tempDir);
Optional<AccessorSummary> child = index.lookup("com.example.Child", "getEvent");
assertThat(child).isPresent();
assertThat(child.orElseThrow().ownerFqn()).isEqualTo("com.example.Child");
}
@Test
void shouldResolveFieldDeclaredInSuperclass(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Base.java", """
package com.example;
public class Base {
protected String event;
}
""");
writeJava(tempDir, "com/example/Child.java", """
package com.example;
public class Child extends Base {
public String getEvent() { return event; }
}
""");
AccessorIndex index = scan(tempDir);
AccessorSummary summary = index.lookup("com.example.Child", "getEvent").orElseThrow();
assertThat(summary.declaringFqn()).isEqualTo("com.example.Base");
}
}
@Nested
class Records {
@Test
void shouldIndexRecordComponents(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Event.java", """
package com.example;
public record Event(String code, int version) {}
""");
AccessorIndex index = scan(tempDir);
assertThat(index.lookup("com.example.Event", "code")).isPresent();
assertThat(index.lookup("com.example.Event", "code").orElseThrow().kind())
.isEqualTo(AccessorKind.RECORD_COMPONENT);
assertThat(index.lookup("com.example.Event", "version")).isPresent();
}
}
@Nested
class Interfaces {
@Test
void shouldIndexDefaultInterfaceAccessor(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/HasEvent.java", """
package com.example;
public interface HasEvent {
default String getEvent() {
return null;
}
}
""");
AccessorIndex index = scan(tempDir);
// Default method body is not trivial (returns null literal, not field read)
assertThat(index.lookup("com.example.HasEvent", "getEvent")).isEmpty();
}
@Test
void shouldIndexTrivialDefaultInterfaceAccessor(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Holder.java", """
package com.example;
public interface Holder {
String getEvent();
}
""");
writeJava(tempDir, "com/example/Impl.java", """
package com.example;
public class Impl implements Holder {
private String event;
public String getEvent() { return event; }
}
""");
AccessorIndex index = scan(tempDir);
assertThat(index.lookup("com.example.Impl", "getEvent")).isPresent();
assertThat(index.lookup("com.example.Holder", "getEvent")).isEmpty();
}
}
@Nested
class Enums {
@Test
void shouldIndexEnumFieldGetter(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Status.java", """
package com.example;
public enum Status {
OK;
private final String code = "ok";
public String getCode() { return code; }
}
""");
AccessorIndex index = scan(tempDir);
assertThat(index.lookup("com.example.Status", "getCode")).isPresent();
}
}
@Test
void shouldBuildViaCodebaseContext(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Demo.java", """
package com.example;
public class Demo {
private String type;
public String getType() { return type; }
}
""");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.getAccessorIndex().lookup("com.example.Demo", "getType")).isPresent();
assertThat(context.getAccessorIndex().trivialCount()).isGreaterThan(0);
}
private static AccessorIndex scan(Path tempDir) throws IOException {
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
return context.getAccessorIndex();
}
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
Path file = tempDir.resolve(relativePath);
Files.createDirectories(file.getParent());
Files.writeString(file, source);
}
}

View File

@@ -0,0 +1,618 @@
package click.kamil.springstatemachineexporter.analysis.index;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.ast.common.DataFlowModel;
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.junit.jupiter.api.Nested;
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 AccessorInliningDataFlowTest {
@Nested
class GetterInlining {
@Test
void shouldResolveTrivialGetterThroughFieldBinding(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Payload {
private String event = "SHIP";
public String getEvent() { return event; }
}
public void run() {
Payload payload = new Payload();
String value = payload.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "SHIP");
assertAccessorIndexed(tempDir, "com.example.Runner.Payload", "getEvent");
}
@Test
void shouldResolveChainedTrivialGetters(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Event {
private String code = "PAY";
public String getCode() { return code; }
}
public static class Payload {
private Event event = new Event();
public Event getEvent() { return event; }
}
public static class Order {
private Payload payload = new Payload();
public Payload getPayload() { return payload; }
}
public void run() {
Order order = new Order();
String code = order.getPayload().getEvent().getCode();
}
}
""");
assertResolvedValue(tempDir, "code", "PAY");
}
@Test
void shouldResolveInheritedFieldThroughChildGetter(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Base.java", """
package com.example;
public class Base {
protected String event = "BASE";
}
""");
writeJava(tempDir, "com/example/Child.java", """
package com.example;
public class Child extends Base {
public String getEvent() { return event; }
}
""");
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Child child = new Child();
String value = child.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "BASE");
}
@Test
void shouldResolveRecordComponentAfterNonTrivialGetterWrapper(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Event.java", """
package com.example;
public record Event(String code) {}
""");
writeJava(tempDir, "com/example/Payload.java", """
package com.example;
public class Payload {
private Event event = new Event("WRAPPED");
public Event getEvent() { return event; }
public Event getEventViaWrapper() { return getEvent(); }
}
""");
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Payload payload = new Payload();
String code = payload.getEventViaWrapper().code();
}
}
""");
assertResolvedValue(tempDir, "code", "WRAPPED");
}
@Test
void shouldResolveRecordComponentThroughAccessorChain(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Event.java", """
package com.example;
public record Event(String code) {}
""");
writeJava(tempDir, "com/example/Payload.java", """
package com.example;
public class Payload {
private Event event = new Event("RECORD");
public Event getEvent() { return event; }
}
""");
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Payload payload = new Payload();
String code = payload.getEvent().code();
}
}
""");
assertResolvedValue(tempDir, "code", "RECORD");
}
}
@Nested
class SetterThenGetter {
@Test
void shouldApplySetterMutationBeforeGetterRead(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Command {
private String event = "DEFAULT";
public void setEvent(String event) { this.event = event; }
public String getEvent() { return this.event; }
}
public void run() {
Command cmd = new Command();
cmd.setEvent("PAY");
String value = cmd.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "PAY");
}
@Test
void shouldApplySetterThroughInterfaceTypedReceiver(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public interface Command {
void setEvent(String event);
String getEvent();
}
public static class Impl implements Command {
private String event = "DEFAULT";
@Override
public void setEvent(String event) { this.event = event; }
@Override
public String getEvent() { return event; }
}
public void run() {
Command cmd = new Impl();
cmd.setEvent("PAY");
String value = cmd.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "PAY");
}
@Test
void shouldResolveSetterArgumentThroughInterfaceTypedReceiver(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public interface Command {
void setEvent(String event);
String getEvent();
}
public static class Impl implements Command {
private String event = "DEFAULT";
@Override
public void setEvent(String event) { this.event = event; }
@Override
public String getEvent() { return event; }
}
public static class Helper {
public String constant() { return "PAY"; }
}
public void run() {
Command cmd = new Impl();
Helper helper = new Helper();
cmd.setEvent(helper.constant());
String value = cmd.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "PAY");
}
@Test
void shouldResolveFluentSetterChain(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Builder {
private String event = "INIT";
public Builder setEvent(String event) {
this.event = event;
return this;
}
public String getEvent() { return event; }
}
public void run() {
Builder builder = new Builder();
builder.setEvent("FLUENT");
String value = builder.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "FLUENT");
}
}
@Nested
class MustNotBreakExistingResolution {
@Test
void shouldStillResolveLiteralReturnWhenGetterIsNotTrivial(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Service {
public String getEvent() { return "LITERAL"; }
}
public void run() {
Service service = new Service();
String value = service.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "LITERAL");
assertThat(accessorLookup(tempDir, "com.example.Runner.Service", "getEvent")).isEmpty();
}
@Test
void shouldStillResolveDynamicDispatchWhenImplementationReturnsLiteral(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public interface Service {
String getEvent();
}
public static class PayService implements Service {
public String getEvent() { return "PAY"; }
}
public void run() {
Service service = new PayService();
String value = service.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "PAY");
}
@Test
void shouldNotInlineParentAccessorWhenSubclassOverrideIsBlocked(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Base.java", """
package com.example;
public class Base {
protected String event = "base";
public String getEvent() { return event; }
}
""");
writeJava(tempDir, "com/example/Child.java", """
package com.example;
public class Child extends Base {
@Override
public String getEvent() {
return "child";
}
}
""");
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Base target = new Child();
String value = target.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "child");
}
@Test
void shouldApplySetterMutationsOnInstanceFieldReceiver(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
private Command command = new Command();
public void run() {
this.command.setEvent("PAY");
String value = this.command.getEvent();
}
public static class Command {
private String event = "DEFAULT";
public void setEvent(String event) { this.event = event; }
public String getEvent() { return event; }
}
}
""");
assertResolvedValue(tempDir, "value", "PAY");
}
@Test
void shouldNotApplyParentAccessorIndexWhenChildOverrideIsBlocked(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Base.java", """
package com.example;
public class Base {
protected String event = "BASE";
public String getEvent() { return event; }
}
""");
writeJava(tempDir, "com/example/Child.java", """
package com.example;
public class Child extends Base {
@Override
public String getEvent() {
return event == null ? "NONE" : event.toUpperCase();
}
}
""");
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Child child = new Child();
child.event = "pay";
String value = child.getEvent();
}
}
""");
assertThat(accessorLookup(tempDir, "com.example.Child", "getEvent")).isEmpty();
assertThat(accessorLookup(tempDir, "com.example.Base", "getEvent")).isPresent();
CodebaseContext context = scan(tempDir);
DataFlowModel model = new JdtDataFlowModel(context);
Expression initializer = findInitializer(context, "run", "value");
String resolved = model.resolveValue(initializer, context);
assertThat(resolved).isNotEqualTo("BASE");
}
@Test
void shouldNotConfuseMapGetWithBeanGetter(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
import java.util.Map;
import java.util.HashMap;
public class Runner {
public void run() {
Map<String, String> map = new HashMap<>();
map.put("event", "MAP_VALUE");
String value = map.get("event");
}
}
""");
assertThat(accessorLookup(tempDir, "java.util.HashMap", "get")).isEmpty();
// Map resolution is separate; ensure we do not crash or return wrong constant.
CodebaseContext context = scan(tempDir);
DataFlowModel model = new JdtDataFlowModel(context);
Expression initializer = findInitializer(context, "run", "value");
List<Expression> defs = model.getReachingDefinitions(initializer);
assertThat(defs).isNotEmpty();
}
@Test
void shouldPreserveBranchSensitiveSetterVisibility(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Command {
private String event = "DEFAULT";
public void setEvent(String event) { this.event = event; }
public String getEvent() { return this.event; }
}
public void run(boolean pay) {
Command cmd = new Command();
if (pay) {
cmd.setEvent("PAY");
} else {
cmd.setEvent("CANCEL");
}
String value = cmd.getEvent();
}
}
""");
CodebaseContext context = scan(tempDir);
DataFlowModel model = new JdtDataFlowModel(context);
Expression initializer = findInitializer(context, "run", "value");
String resolved = model.resolveValue(initializer, context);
assertThat(resolved).isIn("PAY", "CANCEL", null);
}
}
@Nested
class SchemaStressCases {
@Test
void shouldHandleDeepGetterChainWithoutStackOverflow(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class L4 { private String v = "DEEP"; public String getV() { return v; } }
public static class L3 { private L4 l4 = new L4(); public L4 getL4() { return l4; } }
public static class L2 { private L3 l3 = new L3(); public L3 getL3() { return l3; } }
public static class L1 { private L2 l2 = new L2(); public L2 getL2() { return l2; } }
public void run() {
L1 root = new L1();
String value = root.getL2().getL3().getL4().getV();
}
}
""");
assertResolvedValue(tempDir, "value", "DEEP");
}
@Test
void shouldHandleMixedTrivialAndLiteralGetterChain(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Inner {
public String code() { return "INNER"; }
}
public static class Outer {
private Inner inner = new Inner();
public Inner getInner() { return inner; }
}
public void run() {
Outer outer = new Outer();
String value = outer.getInner().code();
}
}
""");
assertResolvedValue(tempDir, "value", "INNER");
}
@Test
void shouldResolveSuperclassFieldViaSubclassThisGetter(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Base {
protected String event = "SUPER";
}
public static class Sub extends Base {
public String getEvent() { return event; }
}
public void run() {
Sub sub = new Sub();
String value = sub.getEvent();
}
}
""");
assertResolvedValue(tempDir, "value", "SUPER");
}
@Test
void shouldReturnEmptyWhenAccessorIndexHasNoEntry(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public static class Worker {
public String compute() { return "WORK"; }
}
public void run() {
Worker worker = new Worker();
String value = worker.compute();
}
}
""");
assertResolvedValue(tempDir, "value", "WORK");
assertThat(accessorLookup(tempDir, "com.example.Runner.Worker", "compute")).isEmpty();
}
}
private static void assertResolvedValue(Path tempDir, String variableName, String expected) throws IOException {
CodebaseContext context = scan(tempDir);
DataFlowModel model = new JdtDataFlowModel(context);
Expression initializer = findInitializer(context, "run", variableName);
String resolved = model.resolveValue(initializer, context);
assertThat(resolved).isEqualTo(expected);
}
private static void assertAccessorIndexed(Path tempDir, String ownerFqn, String methodName) throws IOException {
assertThat(accessorLookup(tempDir, ownerFqn, methodName)).isPresent();
}
private static java.util.Optional<AccessorSummary> accessorLookup(Path tempDir, String ownerFqn, String methodName)
throws IOException {
CodebaseContext context = scan(tempDir);
return context.getAccessorIndex().lookup(ownerFqn, methodName);
}
private static CodebaseContext scan(Path tempDir) throws IOException {
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
return context;
}
private static Expression findInitializer(CodebaseContext context, String methodName, String variableName) {
for (TypeDeclaration type : context.getTypeDeclarations()) {
for (MethodDeclaration method : type.getMethods()) {
if (!method.getName().getIdentifier().equals(methodName) || method.getBody() == null) {
continue;
}
for (Object statementObj : method.getBody().statements()) {
if (statementObj instanceof VariableDeclarationStatement statement) {
for (Object fragmentObj : statement.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment
&& fragment.getName().getIdentifier().equals(variableName)) {
return fragment.getInitializer();
}
}
}
}
}
for (TypeDeclaration nested : type.getTypes()) {
Expression nestedResult = findInitializerInType(nested, methodName, variableName);
if (nestedResult != null) {
return nestedResult;
}
}
}
throw new AssertionError("Initializer not found for " + variableName + " in " + methodName);
}
private static Expression findInitializerInType(TypeDeclaration type, String methodName, String variableName) {
for (MethodDeclaration method : type.getMethods()) {
if (!method.getName().getIdentifier().equals(methodName) || method.getBody() == null) {
continue;
}
for (Object statementObj : method.getBody().statements()) {
if (statementObj instanceof VariableDeclarationStatement statement) {
for (Object fragmentObj : statement.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment
&& fragment.getName().getIdentifier().equals(variableName)) {
return fragment.getInitializer();
}
}
}
}
}
for (TypeDeclaration nested : type.getTypes()) {
Expression result = findInitializerInType(nested, methodName, variableName);
if (result != null) {
return result;
}
}
return null;
}
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
Path file = tempDir.resolve(relativePath);
Files.createDirectories(file.getParent());
Files.writeString(file, source);
}
}

View File

@@ -0,0 +1,86 @@
package click.kamil.springstatemachineexporter.analysis.index;
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.Expression;
import org.junit.jupiter.api.BeforeEach;
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.HashSet;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ConstantExtractorAccessorInliningTest {
private CodebaseContext context;
private ConstantExtractor constantExtractor;
private VariableTracer variableTracer;
@BeforeEach
void setUp(@TempDir Path tempDir) throws IOException {
writeJava(tempDir, "com/example/Payload.java", """
package com.example;
public class Payload {
private String event = "SHIP";
public String getEvent() { return event; }
public void setEvent(String event) { this.event = event; }
}
""");
writeJava(tempDir, "com/example/Runner.java", """
package com.example;
public class Runner {
public void run() {
Payload payload = new Payload();
payload.setEvent("PAY");
String value = payload.getEvent();
}
}
""");
context = new CodebaseContext();
context.setResolveBindings(true);
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
context.scan(tempDir);
constantExtractor = new ConstantExtractor(context, context.getConstantResolver(), mi -> null);
variableTracer = new VariableTracer(context, context.getConstantResolver());
constantExtractor.setVariableTracer(variableTracer);
constantExtractor.setConstructorAnalyzer(new ConstructorAnalyzer(context, context.getConstantResolver()));
}
@Test
void resolveMethodReturnConstantShouldReadIndexedGetterField() {
List<String> constants = constantExtractor.resolveMethodReturnConstant(
"com.example.Payload", "getEvent", 0, new HashSet<>(), null);
assertThat(constants).contains("SHIP");
}
@Test
void traceLocalSetterShouldUseAccessorIndexFieldName() {
Expression result = variableTracer.traceLocalSetter(
"com.example.Runner.run", "payload", "getEvent");
assertThat(result).isNotNull();
assertThat(result.toString()).contains("PAY");
}
@Test
void indexedGetterShouldExistForPayload() {
assertThat(context.getAccessorIndex().lookup("com.example.Payload", "getEvent")).isPresent();
assertThat(context.getAccessorIndex().lookup("com.example.Payload", "setEvent")).isPresent();
}
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
Path file = tempDir.resolve(relativePath);
Files.createDirectories(file.getParent());
Files.writeString(file, source);
}
}

View File

@@ -0,0 +1,210 @@
package click.kamil.springstatemachineexporter.analysis.index;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
class TrivialAccessorDetectorTest {
private final TrivialAccessorDetector detector = new TrivialAccessorDetector();
private static final TrivialAccessorDetector.FieldLookup KNOWN_FIELDS =
fieldName -> Optional.of("com.example.Demo");
@Nested
class Getters {
@Test
void shouldDetectPlainGetter() {
MethodDeclaration md = method("""
public class Demo {
private String event;
public String getEvent() { return this.event; }
}
""", 0);
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
assertThat(summary).isPresent();
assertThat(summary.get().kind()).isEqualTo(AccessorKind.GETTER);
assertThat(summary.get().fieldName()).isEqualTo("event");
assertThat(summary.get().methodName()).isEqualTo("getEvent");
}
@Test
void shouldDetectGetterWithoutThisPrefix() {
MethodDeclaration md = method("""
public class Demo {
private String event;
public String getEvent() { return event; }
}
""", 0);
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isPresent();
}
@Test
void shouldDetectBooleanGetter() {
MethodDeclaration md = method("""
public class Demo {
private boolean active;
public boolean isActive() { return active; }
}
""", 0);
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
assertThat(summary).isPresent();
assertThat(summary.get().kind()).isEqualTo(AccessorKind.BOOLEAN_GETTER);
assertThat(summary.get().fieldName()).isEqualTo("active");
}
@Test
void shouldDetectGetterWithCast() {
MethodDeclaration md = method("""
public class Demo {
private Object event;
public Object getEvent() { return (Object) this.event; }
}
""", 0);
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isPresent();
}
@Test
void shouldRejectLazyInitGetter() {
MethodDeclaration md = method("""
public class Demo {
private String event;
public String getEvent() {
if (event == null) {
event = "default";
}
return event;
}
}
""", 0);
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
}
@Test
void shouldRejectGetterWithMethodCall() {
MethodDeclaration md = method("""
public class Demo {
private String event;
public String getEvent() { return event.trim(); }
}
""", 0);
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
}
@Test
void shouldRejectUnknownField() {
MethodDeclaration md = method("""
public class Demo {
public String getMissing() { return missing; }
}
""", 0);
assertThat(detector.detect("com.example.Demo", md, fieldName -> Optional.empty())).isEmpty();
}
}
@Nested
class Setters {
@Test
void shouldDetectVoidSetter() {
MethodDeclaration md = method("""
public class Demo {
private String event;
public void setEvent(String event) { this.event = event; }
}
""", 0);
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
assertThat(summary).isPresent();
assertThat(summary.get().kind()).isEqualTo(AccessorKind.SETTER);
assertThat(summary.get().paramName()).isEqualTo("event");
assertThat(summary.get().returnsThis()).isFalse();
}
@Test
void shouldDetectFluentSetter() {
MethodDeclaration md = method("""
public class Demo {
private String event;
public Demo setEvent(String event) {
this.event = event;
return this;
}
}
""", 0);
Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);
assertThat(summary).isPresent();
assertThat(summary.get().kind()).isEqualTo(AccessorKind.FLUENT_SETTER);
assertThat(summary.get().returnsThis()).isTrue();
}
@Test
void shouldRejectSetterWithValidation() {
MethodDeclaration md = method("""
public class Demo {
private String event;
public void setEvent(String event) {
java.util.Objects.requireNonNull(event);
this.event = event;
}
}
""", 0);
assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
}
}
@Nested
class Records {
@Test
void shouldDetectRecordComponentAccessor() {
Optional<AccessorSummary> summary = detector.detectRecordComponent("com.example.Event", "code");
assertThat(summary).isPresent();
assertThat(summary.get().kind()).isEqualTo(AccessorKind.RECORD_COMPONENT);
assertThat(summary.get().methodName()).isEqualTo("code");
assertThat(summary.get().fieldName()).isEqualTo("code");
}
}
@Nested
class Naming {
@Test
void shouldDecapitalizeJavaBeanProperty() {
assertThat(TrivialAccessorDetector.decapitalize("Event")).isEqualTo("event");
assertThat(TrivialAccessorDetector.decapitalize("URL")).isEqualTo("URL");
}
}
private static MethodDeclaration method(String classSource, int methodIndex) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(classSource.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
TypeDeclaration td = (TypeDeclaration) cu.types().get(0);
return td.getMethods()[methodIndex];
}
}

View File

@@ -0,0 +1,5 @@
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0