removal and better style

This commit is contained in:
2026-06-07 16:13:11 +02:00
parent 86fb4f260b
commit 728df1d8ba
20 changed files with 447 additions and 1567 deletions

View File

@@ -1,6 +1,9 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.Action;
import click.kamil.springstatemachineexporter.ast.app.domain.Guard;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.app.domain.TransitionType;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
@@ -27,7 +30,7 @@ class AstTransitionParserTest {
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo("withExternal");
.isEqualTo(TransitionType.EXTERNAL);
assertThat(transition.getSourceStates()).containsExactly("CREATED");
assertThat(transition.getTargetStates()).containsExactly("PAID");
assertThat(transition)
@@ -50,10 +53,12 @@ class AstTransitionParserTest {
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getGuard)
assertThat(transition.getGuard())
.extracting(Guard::expression)
.isEqualTo("amount > 0");
assertThat(transition.isLambdaGuard()).isFalse();
assertThat(transition.getGuard())
.extracting(Guard::isLambda)
.isEqualTo(false);
}
@Test
@@ -71,8 +76,8 @@ class AstTransitionParserTest {
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getGuard()).contains("context -> context.getExtendedState().getVariables().get(\"amount\") > 0");
assertThat(transition.isLambdaGuard()).isTrue();
assertThat(transition.getGuard().expression()).contains("context -> context.getExtendedState().getVariables().get(\"amount\") > 0");
assertThat(transition.getGuard().isLambda()).isTrue();
}
@Test
@@ -90,8 +95,12 @@ class AstTransitionParserTest {
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getActions()).containsExactly("processPayment()");
assertThat(transition.getIsLambdaActions()).containsExactly(false);
assertThat(transition.getActions())
.extracting(Action::expression)
.containsExactly("processPayment()");
assertThat(transition.getActions())
.extracting(Action::isLambda)
.containsExactly(false);
}
@Test
@@ -109,9 +118,12 @@ class AstTransitionParserTest {
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getActions()).hasSize(1);
assertThat(transition.getActions().getFirst()).contains("context -> System.out.println(\"Payment processed\")");
assertThat(transition.getIsLambdaActions()).containsExactly(true);
assertThat(transition.getActions())
.extracting(Action::expression)
.anyMatch(s -> s.contains("context -> System.out.println(\"Payment processed\")"));
assertThat(transition.getActions())
.extracting(Action::isLambda)
.containsExactly(true);
}
@Test
@@ -132,7 +144,7 @@ class AstTransitionParserTest {
Transition first = transitions.getFirst();
assertThat(first)
.extracting(Transition::getType)
.isEqualTo("withExternal");
.isEqualTo(TransitionType.EXTERNAL);
assertThat(first.getSourceStates()).containsExactly("CREATED");
assertThat(first.getTargetStates()).containsExactly("PAID");
assertThat(first)
@@ -142,7 +154,7 @@ class AstTransitionParserTest {
Transition second = transitions.get(1);
assertThat(second)
.extracting(Transition::getType)
.isEqualTo("withExternal");
.isEqualTo(TransitionType.EXTERNAL);
assertThat(second.getSourceStates()).containsExactly("PAID");
assertThat(second.getTargetStates()).containsExactly("SHIPPED");
assertThat(second)
@@ -168,11 +180,11 @@ class AstTransitionParserTest {
Transition first = transitions.getFirst();
assertThat(first)
.extracting(Transition::getType)
.isEqualTo("withChoice");
.isEqualTo(TransitionType.CHOICE);
assertThat(first.getSourceStates()).containsExactly("PAID");
assertThat(first.getTargetStates()).containsExactly("SHIPPED");
assertThat(first)
.extracting(Transition::getGuard)
assertThat(first.getGuard())
.extracting(Guard::expression)
.isEqualTo("isExpress()");
assertThat(first)
.extracting(Transition::getOrder)
@@ -181,11 +193,11 @@ class AstTransitionParserTest {
Transition second = transitions.get(1);
assertThat(second)
.extracting(Transition::getType)
.isEqualTo("withChoice");
.isEqualTo(TransitionType.CHOICE);
assertThat(second.getSourceStates()).containsExactly("PAID");
assertThat(second.getTargetStates()).containsExactly("CANCELLED");
assertThat(second)
.extracting(Transition::getGuard)
assertThat(second.getGuard())
.extracting(Guard::expression)
.isEqualTo("isCancelled()");
assertThat(second)
.extracting(Transition::getOrder)
@@ -194,7 +206,7 @@ class AstTransitionParserTest {
Transition third = transitions.get(2);
assertThat(third)
.extracting(Transition::getType)
.isEqualTo("withChoice");
.isEqualTo(TransitionType.CHOICE);
assertThat(third.getSourceStates()).containsExactly("PAID");
assertThat(third.getTargetStates()).containsExactly("PENDING");
assertThat(third)
@@ -218,13 +230,13 @@ class AstTransitionParserTest {
assertThat(transitions).hasSize(3);
assertThat(transitions.getFirst())
.extracting(Transition::getType)
.isEqualTo("withJunction");
.isEqualTo(TransitionType.JUNCTION);
assertThat(transitions.get(1))
.extracting(Transition::getType)
.isEqualTo("withJunction");
.isEqualTo(TransitionType.JUNCTION);
assertThat(transitions.get(2))
.extracting(Transition::getType)
.isEqualTo("withJunction");
.isEqualTo(TransitionType.JUNCTION);
}
@Test
@@ -244,12 +256,14 @@ class AstTransitionParserTest {
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo("withInternal");
.isEqualTo(TransitionType.INTERNAL);
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition)
.extracting(Transition::getEvent)
.isEqualTo("UPDATE");
assertThat(transition.getActions()).containsExactly("updateStatus()");
assertThat(transition.getActions())
.extracting(Action::expression)
.containsExactly("updateStatus()");
}
@Test
@@ -269,7 +283,7 @@ class AstTransitionParserTest {
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo("withLocal");
.isEqualTo(TransitionType.LOCAL);
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition.getTargetStates()).containsExactly("PROCESSING");
assertThat(transition)
@@ -294,7 +308,7 @@ class AstTransitionParserTest {
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo("withFork");
.isEqualTo(TransitionType.FORK);
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition.getTargetStates()).containsExactlyInAnyOrder("SHIPPING", "BILLING");
}
@@ -316,7 +330,7 @@ class AstTransitionParserTest {
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo("withJoin");
.isEqualTo(TransitionType.JOIN);
assertThat(transition.getSourceStates()).containsExactlyInAnyOrder("SHIPPING", "BILLING");
assertThat(transition.getTargetStates()).containsExactly("COMPLETED");
}
@@ -416,9 +430,15 @@ class AstTransitionParserTest {
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(3);
assertThat(transitions.getFirst().getActions()).containsExactly("shipExpress()");
assertThat(transitions.get(1).getActions()).containsExactly("cancelOrder()");
assertThat(transitions.get(2).getActions()).containsExactly("defaultAction()");
assertThat(transitions.getFirst().getActions())
.extracting(Action::expression)
.containsExactly("shipExpress()");
assertThat(transitions.get(1).getActions())
.extracting(Action::expression)
.containsExactly("cancelOrder()");
assertThat(transitions.get(2).getActions())
.extracting(Action::expression)
.containsExactly("defaultAction()");
}
@Test
@@ -436,8 +456,10 @@ class AstTransitionParserTest {
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.isLambdaGuard()).isTrue();
assertThat(transition.getIsLambdaActions()).containsExactly(true);
assertThat(transition.getGuard().isLambda()).isTrue();
assertThat(transition.getActions())
.extracting(Action::isLambda)
.containsExactly(true);
}
@Test
@@ -465,7 +487,9 @@ class AstTransitionParserTest {
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getIsLambdaActions()).containsExactly(true);
assertThat(transition.getActions())
.extracting(Action::isLambda)
.containsExactly(true);
}
@Test

View File

@@ -272,6 +272,172 @@ class StateMachineAggregatorTest {
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E2"));
}
@Test
void shouldIgnoreStatesInsideFork() throws IOException {
String source = """
package com.example;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
public class ForkStateConfig extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states) throws Exception {
states
.withStates()
.initial("CREATED")
.fork("PROCESSING")
.and()
.withStates()
.parent("PROCESSING")
.initial("SHIPPING")
.initial("BILLING")
.end("COMPLETED");
}
}
""";
writeClass("ForkStateConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("ForkStateConfig");
aggregator.aggregateStates(td);
assertThat(aggregator.getInitialStates()).containsExactly("CREATED");
assertThat(aggregator.getEndStates()).containsExactly("COMPLETED");
}
@Test
void shouldHandleNestedForkAndRegion() throws IOException {
String source = """
package com.example;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
public class ComplexStateConfig extends StateMachineConfigurerAdapter {
@Override
public void configure(StateMachineStateConfigurer states) throws Exception {
states
.withStates()
.initial("CREATED")
.fork("PROCESSING")
.and()
.withStates()
.parent("PROCESSING")
.region("SHIPPING")
.and()
.withStates()
.parent("SHIPPING")
.initial("PREPARING")
.end("SHIPPED")
.and()
.withStates()
.parent("PROCESSING")
.region("BILLING")
.and()
.withStates()
.parent("BILLING")
.initial("CALCULATING")
.end("BILLED")
.and()
.withStates()
.parent("PROCESSING")
.end("COMPLETED");
}
}
""";
writeClass("ComplexStateConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("ComplexStateConfig");
aggregator.aggregateStates(td);
assertThat(aggregator.getInitialStates()).containsExactly("CREATED");
assertThat(aggregator.getEndStates()).containsExactlyInAnyOrder("COMPLETED", "SHIPPED", "BILLED");
}
@Test
void shouldFollowMethodCallsInOtherClasses() throws IOException {
String otherClassSource = """
package com.example;
public class ExternalTransitions {
public static void addCommon(StateMachineTransitionConfigurer transitions) {
transitions.withExternal().source("S1").target("S2").event("EXT_E1");
}
}
""";
String mainSource = """
package com.example;
public class MainConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
ExternalTransitions.addCommon(transitions);
}
}
""";
writeClass("ExternalTransitions", otherClassSource);
writeClass("MainConfig", mainSource);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("MainConfig");
List<Transition> transitions = aggregator.aggregateTransitions(td);
// Currently StateMachineAggregator only searches within the hierarchy of the searchStartClass.
// It might not find ExternalTransitions.addCommon unless it's static or we handle imports.
// Let's see if it works or if we need to improve the aggregator.
assertThat(transitions).hasSize(1);
assertThat(transitions.get(0).getEvent()).isEqualTo("EXT_E1");
}
@Test
void shouldHandleRecursionSafety() throws IOException {
String source = """
package com.example;
public class RecursiveConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions.withExternal().source("S1").target("S2").event("E1");
recursiveMethod(transitions);
}
private void recursiveMethod(StateMachineTransitionConfigurer transitions) {
transitions.withExternal().source("S2").target("S3").event("E2");
recursiveMethod(transitions);
}
}
""";
writeClass("RecursiveConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("RecursiveConfig");
List<Transition> transitions = aggregator.aggregateTransitions(td);
// Should not crash and should find both transitions
assertThat(transitions).hasSize(2);
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E1"));
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E2"));
}
@Test
void shouldParseTransitionsInsideTryBlock() throws IOException {
String source = """
package com.example;
public class TryConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
try {
transitions.withExternal().source("S1").target("S2").event("TRY_E1");
} catch (Exception e) {
// ignore
}
}
}
""";
writeClass("TryConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("TryConfig");
List<Transition> transitions = aggregator.aggregateTransitions(td);
assertThat(transitions).hasSize(1);
assertThat(transitions.get(0).getEvent()).isEqualTo("TRY_E1");
}
private void writeClass(String name, String source) throws IOException {
Files.writeString(tempDir.resolve(name + ".java"), source);
}

View File

@@ -1,392 +0,0 @@
package click.kamil.springstatemachineexporter.ast.app;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class StateMachineStateConfigurationMethodFinderTest {
@Test
void shouldParseInitialStates() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldParseMultipleInitialStates() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.initial(OrderStates.PENDING)
.end(OrderStates.COMPLETED)
.end(OrderStates.CANCELLED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PENDING");
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.CANCELLED");
}
@Test
void shouldIgnoreStatesInsideFork() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.fork(OrderStates.PROCESSING)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.initial(OrderStates.SHIPPING)
.initial(OrderStates.BILLING)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldIgnoreStatesInsideRegion() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.region(OrderStates.PROCESSING)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.initial(OrderStates.SHIPPING)
.initial(OrderStates.BILLING)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldParseStatesWithStringLiterals() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("CREATED")
.end("COMPLETED");
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("CREATED");
assertThat(visitor.getEndStates()).containsExactly("COMPLETED");
}
@Test
void shouldParseStatesWithQuotedStrings() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("CREATED")
.end("COMPLETED");
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("CREATED");
assertThat(visitor.getEndStates()).containsExactly("COMPLETED");
}
@Test
void shouldIgnoreNonConfigureMethods() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
public void someOtherMethod() {
states
.withStates()
.initial(OrderStates.CREATED);
}
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.PENDING)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.PENDING");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldHandleEmptyConfigureMethod() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
// Empty method
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).isEmpty();
assertThat(visitor.getEndStates()).isEmpty();
}
@Test
void shouldHandleConfigureMethodWithoutWithStates() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
System.out.println("No state configuration here");
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).isEmpty();
assertThat(visitor.getEndStates()).isEmpty();
}
@Test
void shouldParseComplexStateConfiguration() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.state(OrderStates.PAID)
.state(OrderStates.SHIPPED)
.end(OrderStates.COMPLETED)
.end(OrderStates.CANCELLED)
.and()
.withStates()
.parent(OrderStates.PAID)
.initial(OrderStates.PROCESSING)
.end(OrderStates.READY);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PROCESSING");
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.CANCELLED", "OrderStates.READY");
}
@Test
void shouldParseStatesWithMethodCalls() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(getInitialState())
.end(getEndState());
}
private OrderStates getInitialState() {
return OrderStates.CREATED;
}
private OrderStates getEndState() {
return OrderStates.COMPLETED;
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("getInitialState()");
assertThat(visitor.getEndStates()).containsExactly("getEndState()");
}
@Test
void shouldHandleMultipleConfigureMethods() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.end(OrderStates.COMPLETED);
}
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// This should be ignored
states
.withStates()
.initial(OrderStates.PENDING);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PENDING");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldParseStatesWithEnumValues() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.state(OrderStates.PAID)
.state(OrderStates.SHIPPED)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldHandleNestedForkAndRegion() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.fork(OrderStates.PROCESSING)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.region(OrderStates.SHIPPING)
.and()
.withStates()
.parent(OrderStates.SHIPPING)
.initial(OrderStates.PREPARING)
.end(OrderStates.SHIPPED)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.region(OrderStates.BILLING)
.and()
.withStates()
.parent(OrderStates.BILLING)
.initial(OrderStates.CALCULATING)
.end(OrderStates.BILLED)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.SHIPPED", "OrderStates.BILLED");
}
}

View File

@@ -1,344 +0,0 @@
package click.kamil.springstatemachineexporter.ast.app;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class StateMachineTransitionConfigurationMethodFinderTest {
@Test
void shouldFindConfigureMethodInEnumStateMachineConfigurerAdapter() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
transitions
.withExternal()
.source(OrderStates.CREATED)
.target(OrderStates.PAID)
.event(OrderEvents.PAY);
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldFindConfigureMethodInStateMachineConfigurerAdapter() {
String source = """
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("CREATED")
.target("PAID")
.event("PAY");
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldReturnNull_whenClassDoesNotExtendStateMachineConfigurer() {
String source = """
public class OrderStateMachineConfig {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenClassIsInterface() {
String source = """
public interface OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception;
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodIsNotPublic() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
protected void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodDoesNotReturnVoid() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public String configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// This should not be found
return "result";
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodHasWrongNumberOfParameters() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure() throws Exception {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodParameterIsNotStateMachineTransitionConfigurer() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodDoesNotThrowException() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodThrowsDifferentException() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws RuntimeException {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldFindConfigureMethodWithMultipleExceptions() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception, RuntimeException {
transitions
.withExternal()
.source(OrderStates.CREATED)
.target(OrderStates.PAID)
.event(OrderEvents.PAY);
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldFindConfigureMethodWithParameterizedType() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
transitions
.withExternal()
.source(OrderStates.CREATED)
.target(OrderStates.PAID)
.event(OrderEvents.PAY);
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldReturnNull_whenMultipleConfigureMethodsExist() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
// State configuration
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// Transition configuration
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldHandleClassWithNoMethods() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
// No methods
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldHandleClassWithOtherMethods() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
public void someOtherMethod() {
// This should be ignored
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// This should be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldHandleClassWithGenericParameters() {
String source = """
public class OrderStateMachineConfig<S, E> extends EnumStateMachineConfigurerAdapter<S, E> {
@Override
public void configure(StateMachineTransitionConfigurer<S, E> transitions) throws Exception {
// Generic configuration
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldHandleClassWithAnnotations() {
String source = """
@Configuration
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
@Bean
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// Configuration with annotations
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
}

View File

@@ -97,4 +97,53 @@ class CodebaseContextTest {
assertThat(entryPoints).hasSize(1);
assertThat(entryPoints.get(0).getName().getIdentifier()).isEqualTo("LeafConfig");
}
@Test
void shouldFindBeanMethodsReturningStateMachine() throws IOException {
String source = """
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.statemachine.StateMachine;
public class MyApp {
@Bean
public StateMachine<String, String> stateMachine() {
return null;
}
public StateMachine<String, String> notABean() {
return null;
}
}
""";
Files.writeString(tempDir.resolve("MyApp.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
var beanMethods = context.findBeanMethodsReturning(java.util.Set.of("StateMachine"));
assertThat(beanMethods).hasSize(1);
assertThat(beanMethods.get(0).getName().getIdentifier()).isEqualTo("stateMachine");
}
@Test
void shouldFindBeanMethodsReturningStateMachineWithQualifiedName() throws IOException {
String source = """
package com.example;
import org.springframework.context.annotation.Bean;
public class MyApp {
@Bean
public org.springframework.statemachine.StateMachine stateMachine() {
return null;
}
}
""";
Files.writeString(tempDir.resolve("MyApp.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
var beanMethods = context.findBeanMethodsReturning(java.util.Set.of("StateMachine"));
assertThat(beanMethods).hasSize(1);
assertThat(beanMethods.get(0).getName().getIdentifier()).isEqualTo("stateMachine");
}
}

View File

@@ -1,314 +0,0 @@
package click.kamil.springstatemachineexporter.ast.in;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
class AstFileFinderTest {
private static final String CLASS_WITH_ANNOTATION = """
@Service
public class TestService {
public void doSomething() {
// implementation
}
}
""";
private static final String CLASS_WITH_MULTIPLE_ANNOTATIONS = """
@Service
@Component
public class TestService {
public void doSomething() {
// implementation
}
}
""";
private static final String INTERFACE_WITH_ANNOTATION = """
@FunctionalInterface
public interface TestInterface {
void doSomething();
}
""";
private static final String CLASS_WITHOUT_ANNOTATION = """
public class TestService {
public void doSomething() {
// implementation
}
}
""";
private static final String CLASS_WITH_DIFFERENT_ANNOTATION = """
@Repository
public class TestRepository {
public void doSomething() {
// implementation
}
}
""";
private static final String CLASS_WITH_FULLY_QUALIFIED_ANNOTATION = """
@org.springframework.stereotype.Service
public class TestService {
public void doSomething() {
// implementation
}
}
""";
private static final String MULTIPLE_CLASSES = """
@Service
public class TestService {
public void doSomething() {
// implementation
}
}
public class AnotherClass {
// no annotation
}
""";
private static final String CLASS_WITH_ANNOTATION_AND_FIELDS = """
@Service
public class TestService {
private String name;
public void doSomething() {
// implementation
}
}
""";
private static final String EMPTY_SOURCE = "";
private static final String INVALID_JAVA_SYNTAX = """
@Service
public class TestService {
// missing closing brace
""";
@Test
void shouldReturnTrue_whenClassHasMatchingAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnTrue_whenClassHasMultipleMatchingAnnotations() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service", "Component");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_MULTIPLE_ANNOTATIONS, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnFalse_whenClassHasNoAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITHOUT_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenClassHasDifferentAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_DIFFERENT_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnTrue_whenClassHasFullyQualifiedAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("org.springframework.stereotype.Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_FULLY_QUALIFIED_ANNOTATION, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnTrue_whenMultipleClassesAndOneHasAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(MULTIPLE_CLASSES, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnFalse_whenInterfaceHasAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("FunctionalInterface");
boolean result = finder.hasClassWithAnnotation(INTERFACE_WITH_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenEmptyAnnotationList() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = Collections.emptyList();
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenEmptySource() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(EMPTY_SOURCE, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldThrowException_whenNullAnnotationList() {
AstFileFinder finder = new AstFileFinder();
// when & then
assertThatThrownBy(() ->
finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, null)
).isInstanceOf(NullPointerException.class);
}
@Test
void shouldReturnTrue_whenClassHasAnnotationAndFields() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION_AND_FIELDS, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldHandleInvalidJavaSyntax() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(INVALID_JAVA_SYNTAX, annotationNames);
// AST parser might still create a valid AST even with syntax errors
// The result depends on how the parser handles the invalid syntax
assertThat(result).isTrue();
}
@Test
void shouldReturnTrue_whenAnnotationNameMatchesCaseInsensitive() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("service"); // lowercase
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isFalse(); // AST parser is case-sensitive
}
@Test
void shouldReturnTrue_whenMultipleAnnotationNamesProvided() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Component", "Service", "Repository");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnFalse_whenNoMatchingAnnotationNames() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Component", "Repository", "Controller");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenSourceContainsOnlyComments() {
String sourceWithComments = """
// This is a comment
/* Another comment */
/** JavaDoc comment */
""";
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(sourceWithComments, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenSourceContainsOnlyPackageDeclaration() {
String sourceWithPackage = "package com.example;";
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(sourceWithPackage, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnTrue_whenFunctionReturnsTargetType() {
String source = """
public class Test {
@Bean
public StateMachine<S, E> build() { return null; }
}
""";
AstFileFinder finder = new AstFileFinder();
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
assertThat(result).isTrue();
}
@Test
void shouldReturnFalse_whenNoFunctionReturnsTargetType() {
String source = """
public class Test {
@Bean
public void build() {}
}
""";
AstFileFinder finder = new AstFileFinder();
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
assertThat(result).isFalse();
}
@Test
void shouldReturnTrue_whenFunctionReturnsQualifiedTargetType() {
String source = """
public class Test {
@Bean
public org.springframework.statemachine.StateMachine build() { return null; }
}
""";
AstFileFinder finder = new AstFileFinder();
boolean result = finder.hasFunctionReturningType(source, Set.of("StateMachine"));
assertThat(result).isTrue();
}
}