Extract machine routing helpers and gate Spring DI to ambiguous generics.

Keep package heuristics for single-machine codebases and fail closed only when String/Object generic triggers are provably ambiguous across multiple beans.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 20:39:51 +02:00
parent 423bb4e819
commit 60b5ceea8c
6 changed files with 373 additions and 72 deletions

View File

@@ -0,0 +1,114 @@
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SpringInjectionRoutingTest {
@Test
void shouldRouteByQualifierWithoutPackageNameHeuristics(@TempDir Path tempDir) throws Exception {
Files.writeString(tempDir.resolve("PaymentService.java"), """
package com.example;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
@Qualifier("paymentStateMachine")
private final StateMachine<String, String> stateMachine;
public PaymentService(StateMachine<String, String> stateMachine) {
this.stateMachine = stateMachine;
}
public void capture() {
stateMachine.sendEvent("CAPTURE");
}
}
@org.springframework.context.annotation.Configuration
@org.springframework.statemachine.config.EnableStateMachine(name = "paymentStateMachine")
class PaymentStateMachineConfig
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
@org.springframework.context.annotation.Configuration
@org.springframework.statemachine.config.EnableStateMachine(name = "orderStateMachine")
class OrderStateMachineConfig
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
""");
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.className("com.example.PaymentService")
.methodName("capture")
.lineNumber(12)
.event("CAPTURE")
.eventTypeFqn("java.lang.String")
.stateTypeFqn("java.lang.String")
.stateMachineId("paymentStateMachine")
.build())
.methodChain(List.of("com.example.PaymentService.capture()"))
.build();
assertThat(engine.isRoutedToCorrectMachine(
chain, "com.example.PaymentStateMachineConfig", context)).isTrue();
assertThat(engine.isRoutedToCorrectMachine(
chain, "com.example.OrderStateMachineConfig", context)).isFalse();
}
@Test
void shouldFailClosedForAmbiguousStringStateMachineWithoutQualifier(@TempDir Path tempDir) throws Exception {
Files.writeString(tempDir.resolve("OrderService.java"), """
package com.example;
import org.springframework.statemachine.StateMachine;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class OrderService {
private final StateMachine<String, String> stateMachine;
public void finish() {
stateMachine.sendEvent("FINISH");
}
}
@org.springframework.context.annotation.Configuration
@org.springframework.statemachine.config.EnableStateMachine(name = "machineA")
class MachineAConfig
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
@org.springframework.context.annotation.Configuration
@org.springframework.statemachine.config.EnableStateMachine(name = "machineB")
class MachineBConfig
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
""");
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("finish")
.lineNumber(11)
.event("FINISH")
.eventTypeFqn("java.lang.String")
.stateTypeFqn("java.lang.String")
.build())
.methodChain(List.of("com.example.OrderService.finish()"))
.build();
assertThat(engine.isRoutedToCorrectMachine(chain, "com.example.MachineAConfig", context)).isFalse();
assertThat(engine.isRoutedToCorrectMachine(chain, "com.example.MachineBConfig", context)).isFalse();
}
}