Fix CIC accessor resolution, concrete overrides, and reactive flatMap tracing.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||
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.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InheritanceAndNestedResolutionTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
CodebaseContext context;
|
||||
ConstantExtractor constantExtractor;
|
||||
ConstructorAnalyzer constructorAnalyzer;
|
||||
AccessorResolver accessorResolver;
|
||||
VariableTracer variableTracer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||
ConstantResolver constantResolver = context.getConstantResolver();
|
||||
constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
|
||||
constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
|
||||
variableTracer = new VariableTracer(context, constantResolver);
|
||||
constantExtractor.setVariableTracer(variableTracer);
|
||||
constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
|
||||
constructorAnalyzer.setVariableTracer(variableTracer);
|
||||
constructorAnalyzer.setConstantExtractor(constantExtractor);
|
||||
accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
||||
constantExtractor.setAccessorResolver(accessorResolver);
|
||||
}
|
||||
|
||||
private void writeJava(String relativePath, String source) throws IOException {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, source);
|
||||
}
|
||||
|
||||
private void scan() throws IOException {
|
||||
context.scan(tempDir);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class InterfaceDefaultMethod {
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldIncludeDefaultMethodAndAllImpls() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { DEFAULT, PAY, SHIP }
|
||||
""");
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default Code getCode() { return Code.DEFAULT; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public Code getCode() { return Code.PAY; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public Code getCode() { return Code.SHIP; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
null, null, false, null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("Code.DEFAULT", "Code.PAY", "Code.SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldMergeDefaultMethodWithConcreteOverrides() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default String getCode() { return "DEFAULT"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public String getCode() { return "PAY"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public String getCode() { return "SHIP"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
null, null, false, null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("DEFAULT", "PAY", "SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldPreferConcreteCicOverInterfaceDefault() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { DEFAULT, PAY, SHIP }
|
||||
""");
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default Code getCode() { return Code.DEFAULT; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public Code getCode() { return Code.PAY; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
void run() {
|
||||
EventSource src = new PayEvent();
|
||||
}
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
org.eclipse.jdt.core.dom.CompilationUnit cu = (org.eclipse.jdt.core.dom.CompilationUnit) runner.getRoot();
|
||||
final org.eclipse.jdt.core.dom.ClassInstanceCreation[] payEventCic =
|
||||
new org.eclipse.jdt.core.dom.ClassInstanceCreation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.ClassInstanceCreation node) {
|
||||
if ("PayEvent".equals(node.getType().toString())) {
|
||||
payEventCic[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
payEventCic[0],
|
||||
cu,
|
||||
false,
|
||||
null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactly("Code.PAY");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class NonIndexedConcreteOverride {
|
||||
|
||||
@Test
|
||||
void dataFlowShouldResolveNonTrivialConcreteOverride() throws IOException {
|
||||
writeJava("com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
Payload payload = new CustomPayload();
|
||||
String event = payload.getEvent();
|
||||
}
|
||||
}
|
||||
interface Payload {
|
||||
String getEvent();
|
||||
}
|
||||
class CustomPayload implements Payload {
|
||||
public String getEvent() {
|
||||
return sideEffect();
|
||||
}
|
||||
private String sideEffect() { return "CUSTOM"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
MethodDeclaration run = context.findMethodDeclaration(runner, "run", true);
|
||||
final MethodInvocation[] getterCall = new MethodInvocation[1];
|
||||
run.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getInitializer() instanceof MethodInvocation mi) {
|
||||
getterCall[0] = mi;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
DataFlowModel model = new JdtDataFlowModel(context);
|
||||
String resolved = model.resolveValue(getterCall[0], context);
|
||||
assertThat(resolved).isEqualTo("CUSTOM");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GetterChainThroughInterface {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveDeepChainWithInterfaceMiddleHop() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
CentralDispatcher dispatcher;
|
||||
Outer outer;
|
||||
public void pay() {
|
||||
dispatcher.fire(outer.getInner().getPayload().getEvent());
|
||||
}
|
||||
}
|
||||
class CentralDispatcher {
|
||||
public void fire(OrderEvent event) {}
|
||||
}
|
||||
interface Inner {
|
||||
Payload getPayload();
|
||||
}
|
||||
class InnerImpl implements Inner {
|
||||
private Payload payload = new Payload();
|
||||
public Payload getPayload() { return payload; }
|
||||
}
|
||||
class Outer {
|
||||
public Inner getInner() { return new InnerImpl(); }
|
||||
}
|
||||
class Payload {
|
||||
private OrderEvent event = OrderEvent.PAY;
|
||||
public OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.CentralDispatcher")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.contains("OrderEvent.PAY");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConcreteSubtypeTrigger {
|
||||
|
||||
@Test
|
||||
void callGraphShouldPreferConcreteSubtypeFieldOverAbstractDefault() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
OrderService service;
|
||||
public void ship() {
|
||||
service.fire(new ShipEvent());
|
||||
}
|
||||
}
|
||||
class OrderService {
|
||||
StateMachine sm;
|
||||
public void fire(BaseEvent event) {
|
||||
sm.sendEvent(event.getCode());
|
||||
}
|
||||
}
|
||||
enum Code { PAY, SHIP }
|
||||
abstract class BaseEvent {
|
||||
protected Code code = Code.PAY;
|
||||
public Code getCode() { return code; }
|
||||
}
|
||||
class ShipEvent extends BaseEvent {
|
||||
public ShipEvent() { this.code = Code.SHIP; }
|
||||
}
|
||||
class StateMachine { void sendEvent(Code event) {} }
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("ship")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("Code.SHIP");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConcreteSubtypeTriggerViaFire {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveSubtypeWhenPassedToBaseTypedFire() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
OrderService service;
|
||||
public void ship() {
|
||||
service.fire(new ShipEvent());
|
||||
}
|
||||
}
|
||||
class OrderService {
|
||||
public void fire(BaseEvent event) {}
|
||||
}
|
||||
enum Code { PAY, SHIP }
|
||||
abstract class BaseEvent {
|
||||
protected Code code = Code.PAY;
|
||||
public Code getCode() { return code; }
|
||||
}
|
||||
class ShipEvent extends BaseEvent {
|
||||
public ShipEvent() { this.code = Code.SHIP; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("ship")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.OrderService")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getMethodChain())
|
||||
.containsExactly(
|
||||
"com.example.ApiController.ship",
|
||||
"com.example.OrderService.fire");
|
||||
assertThat(chains.get(0).getTriggerPoint().getEvent())
|
||||
.contains("ShipEvent");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class UnresolvedTypeWidening {
|
||||
|
||||
@Test
|
||||
void constantExtractorShouldWidenWhenTypeDeclarationMissing() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { A, B }
|
||||
""");
|
||||
writeJava("com/example/EventA.java", """
|
||||
package com.example;
|
||||
public class EventA implements RichEvent {
|
||||
public Code getCode() { return Code.A; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/EventB.java", """
|
||||
package com.example;
|
||||
public class EventB implements RichEvent {
|
||||
public Code getCode() { return Code.B; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/RichEvent.java", """
|
||||
package com.example;
|
||||
public interface RichEvent {
|
||||
Code getCode();
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = constantExtractor.resolveMethodReturnConstant(
|
||||
"RichEvent", "getCode", 0, new HashSet<>(), null);
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("Code.A", "Code.B");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DeterministicImplementations {
|
||||
|
||||
@Test
|
||||
void getImplementationsShouldReturnSortedStableOrder() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
String getCode();
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ZebraEvent.java", """
|
||||
package com.example;
|
||||
public class ZebraEvent implements EventSource {
|
||||
public String getCode() { return "Z"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/AlphaEvent.java", """
|
||||
package com.example;
|
||||
public class AlphaEvent implements EventSource {
|
||||
public String getCode() { return "A"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> first = context.getImplementations("com.example.EventSource");
|
||||
List<String> second = context.getImplementations("com.example.EventSource");
|
||||
|
||||
assertThat(first).containsExactly(
|
||||
"com.example.AlphaEvent",
|
||||
"com.example.ZebraEvent");
|
||||
assertThat(second).isEqualTo(first);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ReactiveFlatMapLambdaSendEvent {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterInsideFlatMapLambda() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload).flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
OrderEvent event = OrderEvent.PAY;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterInsideFlatMapBlockBody() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload).flatMap(p -> {
|
||||
sm.sendEvent(p.getEvent());
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
OrderEvent event = OrderEvent.PAY;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
static Mono empty() { return new Mono(); }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterThroughNestedFlatMap() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload)
|
||||
.flatMap(x -> Mono.just(x.getInner()))
|
||||
.flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
Inner inner = new Inner();
|
||||
Inner getInner() { return inner; }
|
||||
}
|
||||
class Inner {
|
||||
OrderEvent event = OrderEvent.SHIP;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.SHIP");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ReactiveExpressionSupportTest {
|
||||
|
||||
@Test
|
||||
void shouldMapLambdaParameterToOuterJustPayload(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
Payload payload;
|
||||
void run() {
|
||||
Mono.just(payload).flatMap(p -> Mono.just(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload { String getEvent() { return "X"; } }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
""";
|
||||
Path file = tempDir.resolve("Runner.java");
|
||||
Files.writeString(file, source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
CompilationUnit cu = (CompilationUnit) runner.getRoot();
|
||||
final MethodInvocation[] flatMapCall = new MethodInvocation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ("flatMap".equals(node.getName().getIdentifier()) && flatMapCall[0] == null) {
|
||||
flatMapCall[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
String payload = ReactiveExpressionSupport.extractPayload(
|
||||
flatMapCall[0], context.getConstantResolver(), context);
|
||||
assertThat(payload).isEqualTo("payload.getEvent()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemapLambdaParameterGetter(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
Payload payload;
|
||||
StateMachine sm;
|
||||
void run() {
|
||||
Mono.just(payload).flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload { String getEvent() { return "X"; } }
|
||||
class StateMachine { void sendEvent(String e) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
""";
|
||||
Path file = tempDir.resolve("Runner.java");
|
||||
Files.writeString(file, source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
CompilationUnit cu = (CompilationUnit) runner.getRoot();
|
||||
final MethodInvocation[] sendEventCall = new MethodInvocation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ("sendEvent".equals(node.getName().getIdentifier()) && sendEventCall[0] == null) {
|
||||
sendEventCall[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
Expression eventExpr = (Expression) sendEventCall[0].arguments().get(0);
|
||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||
eventExpr, context.getConstantResolver(), context);
|
||||
assertThat(remapped).isEqualTo("payload.getEvent()");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TypeResolverTest {
|
||||
|
||||
@Test
|
||||
void shouldMapGenericEventPlaceholderToSingleParameterIndex(@TempDir Path tempDir) throws Exception {
|
||||
Files.writeString(tempDir.resolve("Machine.java"), """
|
||||
package com.example;
|
||||
class StateMachine {
|
||||
void sendEvent(OrderEvent event) {}
|
||||
}
|
||||
enum OrderEvent { PAY }
|
||||
""");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
TypeResolver resolver = new TypeResolver(context);
|
||||
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "event")).isZero();
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "e", true)).isZero();
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "e")).isEqualTo(-1);
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "payload")).isEqualTo(-1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user