Trace Supplier lambda arguments across call frames so JMS cancel chains resolve the concrete event.
When callee code calls eventProvider.get(), walk back to the caller's Supplier argument instead of stopping at the parameter name; also skip Supplier-vs-enum type checks for already-resolved lambda/enum call-site arguments under JDT bindings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
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.spring.InjectionPointAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ComplexMultiModuleCancelJmsTest {
|
||||
|
||||
private CodebaseContext context;
|
||||
private JdtCallGraphEngine engine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
Path projectRoot = Path.of("../state_machines/complex_multi_module_sm").toAbsolutePath().normalize();
|
||||
|
||||
context = new CodebaseContext();
|
||||
context.setProjectRoot(projectRoot);
|
||||
context.setSourcepath(List.of(
|
||||
projectRoot.resolve("domain/src/main/java").toString(),
|
||||
projectRoot.resolve("service/src/main/java").toString(),
|
||||
projectRoot.resolve("web/src/main/java").toString()));
|
||||
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||
context.setResolveBindings(true);
|
||||
context.scan(Set.of(projectRoot), Collections.emptySet());
|
||||
|
||||
SpringBeanRegistry registry = new SpringBeanRegistry();
|
||||
SpringContextScanner scanner = new SpringContextScanner(registry);
|
||||
for (var cu : context.getCompilationUnits()) {
|
||||
cu.accept(scanner);
|
||||
}
|
||||
InjectionPointAnalyzer injectionAnalyzer = new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
|
||||
engine = new JdtCallGraphEngine(context, injectionAnalyzer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveCancelEventFromJmsSupplierLambda() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("click.kamil.web.JmsOrderListener")
|
||||
.methodName("receiveCancelCommand")
|
||||
.build();
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("click.kamil.service.StateMachineServiceImpl")
|
||||
.methodName("sendMessageWithProvider")
|
||||
.event("eventProvider")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.contains("OrderEvent.CANCEL");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
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.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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 CrossClassSupplierLambdaTest {
|
||||
|
||||
@Test
|
||||
void shouldResolveEventThroughCrossClassSupplierLambda() throws IOException {
|
||||
String webSource = """
|
||||
package com.example.web;
|
||||
|
||||
import com.example.service.StateMachineService;
|
||||
import com.example.domain.OrderEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class JmsOrderListener {
|
||||
private final StateMachineService stateMachineService;
|
||||
|
||||
public JmsOrderListener(StateMachineService stateMachineService) {
|
||||
this.stateMachineService = stateMachineService;
|
||||
}
|
||||
|
||||
public void receiveCancelCommand(String message) {
|
||||
stateMachineService.sendMessageWithProvider(() -> OrderEvent.CANCEL);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String serviceSource = """
|
||||
package com.example.service;
|
||||
|
||||
import com.example.domain.OrderEvent;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class StateMachineServiceImpl implements StateMachineService {
|
||||
private StateMachine<OrderEvent, OrderEvent> stateMachine;
|
||||
|
||||
@Override
|
||||
public <T extends OrderEvent> void sendMessageWithProvider(Supplier<T> eventProvider) {
|
||||
T event = eventProvider.get();
|
||||
if (event != null) {
|
||||
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(event).build())).subscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String ifaceSource = """
|
||||
package com.example.service;
|
||||
|
||||
import com.example.domain.OrderEvent;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface StateMachineService {
|
||||
<T extends OrderEvent> void sendMessageWithProvider(Supplier<T> eventProvider);
|
||||
}
|
||||
""";
|
||||
|
||||
String domainSource = """
|
||||
package com.example.domain;
|
||||
|
||||
public enum OrderEvent { CANCEL, PROCESS }
|
||||
""";
|
||||
|
||||
Path tempDir = Files.createTempDirectory("callgraph_cross_class_lambda");
|
||||
Files.writeString(tempDir.resolve("JmsOrderListener.java"), webSource);
|
||||
Files.writeString(tempDir.resolve("StateMachineServiceImpl.java"), serviceSource);
|
||||
Files.writeString(tempDir.resolve("StateMachineService.java"), ifaceSource);
|
||||
Files.writeString(tempDir.resolve("OrderEvent.java"), domainSource);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.web.JmsOrderListener")
|
||||
.methodName("receiveCancelCommand")
|
||||
.build();
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.service.StateMachineServiceImpl")
|
||||
.methodName("sendMessageWithProvider")
|
||||
.event("eventProvider")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.CANCEL");
|
||||
}
|
||||
}
|
||||
@@ -452,7 +452,7 @@
|
||||
},
|
||||
"methodChain" : [ "click.kamil.web.JmsOrderListener.receiveCancelCommand", "click.kamil.service.StateMachineServiceImpl.sendMessageWithProvider" ],
|
||||
"triggerPoint" : {
|
||||
"event" : "eventProvider",
|
||||
"event" : "click.kamil.domain.OrderEvent.CANCEL",
|
||||
"className" : "click.kamil.service.StateMachineServiceImpl",
|
||||
"methodName" : "sendMessageWithProvider",
|
||||
"sourceFile" : "service/src/main/java/click/kamil/service/StateMachineServiceImpl.java",
|
||||
@@ -460,14 +460,14 @@
|
||||
"stateMachineId" : null,
|
||||
"sourceState" : null,
|
||||
"lineNumber" : 50,
|
||||
"polymorphicEvents" : null,
|
||||
"polymorphicEvents" : [ "click.kamil.domain.OrderEvent.CANCEL" ],
|
||||
"external" : false,
|
||||
"constraint" : "event != null",
|
||||
"ambiguous" : false
|
||||
"ambiguous" : true
|
||||
},
|
||||
"contextMachineId" : null,
|
||||
"matchedTransitions" : null,
|
||||
"linkResolution" : "NO_MATCH"
|
||||
"linkResolution" : "AMBIGUOUS_WIDEN"
|
||||
} ],
|
||||
"properties" : {
|
||||
"default" : { }
|
||||
|
||||
Reference in New Issue
Block a user