A
This commit is contained in:
@@ -2,24 +2,45 @@ package click.kamil.springstatemachineexporter.analysis.model;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
|
||||||
public class CallEdge {
|
public class CallEdge {
|
||||||
private String targetMethod;
|
private String targetMethod;
|
||||||
private List<String> arguments;
|
private List<String> arguments;
|
||||||
private String receiver;
|
private String receiver;
|
||||||
private String constraint;
|
private String constraint;
|
||||||
|
/**
|
||||||
|
* Proven or CHA-refined receiver type FQN when known (field/param type, unique injection,
|
||||||
|
* or the concrete impl class of a polymorphic expansion target). Used by pathfinding to
|
||||||
|
* prune collaborator template-method fan-out; null means fail open.
|
||||||
|
*/
|
||||||
|
private String receiverTypeFqn;
|
||||||
|
|
||||||
public CallEdge(String targetMethod, List<String> arguments) {
|
public CallEdge(String targetMethod, List<String> arguments) {
|
||||||
this(targetMethod, arguments, null, null);
|
this(targetMethod, arguments, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CallEdge(String targetMethod, List<String> arguments, String receiver) {
|
public CallEdge(String targetMethod, List<String> arguments, String receiver) {
|
||||||
this(targetMethod, arguments, receiver, null);
|
this(targetMethod, arguments, receiver, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallEdge(String targetMethod, List<String> arguments, String receiver, String constraint) {
|
||||||
|
this(targetMethod, arguments, receiver, constraint, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallEdge(
|
||||||
|
String targetMethod,
|
||||||
|
List<String> arguments,
|
||||||
|
String receiver,
|
||||||
|
String constraint,
|
||||||
|
String receiverTypeFqn) {
|
||||||
|
this.targetMethod = targetMethod;
|
||||||
|
this.arguments = arguments;
|
||||||
|
this.receiver = receiver;
|
||||||
|
this.constraint = constraint;
|
||||||
|
this.receiverTypeFqn = receiverTypeFqn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1375,6 +1375,54 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return receiver.toString();
|
return receiver.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort type of the call receiver for call-graph edges. Implicit {@code this} uses the
|
||||||
|
* enclosing type; simple names use field/param/local resolution.
|
||||||
|
*/
|
||||||
|
protected String resolveReceiverTypeFqn(Expression receiver, TypeDeclaration enclosingType) {
|
||||||
|
if (receiver == null || receiver instanceof ThisExpression) {
|
||||||
|
return enclosingType != null ? context.getFqn(enclosingType) : null;
|
||||||
|
}
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
return resolveReceiverTypeFallback(sn);
|
||||||
|
}
|
||||||
|
if (receiver instanceof FieldAccess fa) {
|
||||||
|
return resolveReceiverTypeFallback(fa.getName());
|
||||||
|
}
|
||||||
|
if (receiver instanceof MethodInvocation mi) {
|
||||||
|
return resolveMethodInvocationReturnType(mi);
|
||||||
|
}
|
||||||
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
return binding.getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For CHA-expanded targets {@code Impl.method}, prefer the concrete impl class as the edge's
|
||||||
|
* receiver type so pathfinding can prune sibling overrides inside that collaborator hierarchy.
|
||||||
|
*/
|
||||||
|
protected String refineReceiverTypeForTarget(String declaredReceiverTypeFqn, String calledMethodFqn) {
|
||||||
|
if (calledMethodFqn == null || !calledMethodFqn.contains(".")) {
|
||||||
|
return declaredReceiverTypeFqn;
|
||||||
|
}
|
||||||
|
String targetClass = calledMethodFqn.substring(0, calledMethodFqn.lastIndexOf('.'));
|
||||||
|
if (targetClass.contains("<")) {
|
||||||
|
targetClass = targetClass.substring(0, targetClass.indexOf('<'));
|
||||||
|
}
|
||||||
|
if (!context.isConcreteClassType(targetClass)) {
|
||||||
|
return declaredReceiverTypeFqn;
|
||||||
|
}
|
||||||
|
if (declaredReceiverTypeFqn == null
|
||||||
|
|| declaredReceiverTypeFqn.isBlank()
|
||||||
|
|| context.isSubtypeOrSame(targetClass, declaredReceiverTypeFqn)
|
||||||
|
|| context.areSameTypeOrUnambiguousSimpleMatch(targetClass, declaredReceiverTypeFqn)) {
|
||||||
|
return targetClass;
|
||||||
|
}
|
||||||
|
return declaredReceiverTypeFqn;
|
||||||
|
}
|
||||||
|
|
||||||
protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
|
protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
|
||||||
int lastDot = callerFqn.lastIndexOf('.');
|
int lastDot = callerFqn.lastIndexOf('.');
|
||||||
if (lastDot < 0) {
|
if (lastDot < 0) {
|
||||||
@@ -2545,9 +2593,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
List<String> args = resolveArguments(node.arguments());
|
List<String> args = resolveArguments(node.arguments());
|
||||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||||
.findConditionConstraint(node);
|
.findConditionConstraint(node);
|
||||||
|
String receiver = getReceiverString(node.getExpression());
|
||||||
|
String declaredReceiverType =
|
||||||
|
resolveReceiverTypeFqn(node.getExpression(), td);
|
||||||
for (String calledMethod : calledMethods) {
|
for (String calledMethod : calledMethods) {
|
||||||
String receiver = getReceiverString(node.getExpression());
|
String receiverTypeFqn =
|
||||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
refineReceiverTypeForTarget(declaredReceiverType, calledMethod);
|
||||||
|
CallEdge edge = new CallEdge(
|
||||||
|
calledMethod, args, receiver, null, receiverTypeFqn);
|
||||||
edge.setConstraint(resolveConstraint(node, calledMethod, constraint));
|
edge.setConstraint(resolveConstraint(node, calledMethod, constraint));
|
||||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||||
}
|
}
|
||||||
@@ -2566,9 +2619,13 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
} else {
|
} else {
|
||||||
implicitArgs.addAll(args);
|
implicitArgs.addAll(args);
|
||||||
}
|
}
|
||||||
String receiver = getReceiverString(node.getExpression());
|
CallEdge edge = new CallEdge(
|
||||||
CallEdge edge = new CallEdge(refMethod, implicitArgs, receiver);
|
refMethod,
|
||||||
edge.setConstraint(constraint);
|
implicitArgs,
|
||||||
|
receiver,
|
||||||
|
constraint,
|
||||||
|
refineReceiverTypeForTarget(
|
||||||
|
declaredReceiverType, refMethod));
|
||||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2589,16 +2646,24 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
} else {
|
} else {
|
||||||
implicitArgs.addAll(args);
|
implicitArgs.addAll(args);
|
||||||
}
|
}
|
||||||
String receiver = getReceiverString(node.getExpression());
|
String emrReceiver = getReceiverString(emr.getExpression());
|
||||||
CallEdge edge = new CallEdge(calledMethod, implicitArgs, receiver);
|
CallEdge edge = new CallEdge(
|
||||||
edge.setConstraint(constraint);
|
calledMethod,
|
||||||
|
implicitArgs,
|
||||||
|
emrReceiver,
|
||||||
|
constraint,
|
||||||
|
refineReceiverTypeForTarget(fallbackTypeFqn, calledMethod));
|
||||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||||
|
|
||||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||||
for (String impl : impls) {
|
for (String impl : impls) {
|
||||||
|
String implMethod = impl + "." + emr.getName().getIdentifier();
|
||||||
CallEdge implEdge = new CallEdge(
|
CallEdge implEdge = new CallEdge(
|
||||||
impl + "." + emr.getName().getIdentifier(), args, receiver);
|
implMethod,
|
||||||
implEdge.setConstraint(constraint);
|
args,
|
||||||
|
emrReceiver,
|
||||||
|
constraint,
|
||||||
|
refineReceiverTypeForTarget(fallbackTypeFqn, implMethod));
|
||||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(implEdge);
|
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(implEdge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2625,8 +2690,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
String receiver = "super";
|
String receiver = "super";
|
||||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||||
.findConditionConstraint(node);
|
.findConditionConstraint(node);
|
||||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
String receiverTypeFqn = refineReceiverTypeForTarget(
|
||||||
edge.setConstraint(constraint);
|
context.getFqn(tdOuter), calledMethod);
|
||||||
|
CallEdge edge = new CallEdge(
|
||||||
|
calledMethod, args, receiver, constraint, receiverTypeFqn);
|
||||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,10 +163,12 @@ public class CallGraphPathFinder {
|
|||||||
|| neighbor.startsWith("org.springframework.")) {
|
|| neighbor.startsWith("org.springframework.")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Prune against the current dispatch self first; only then adopt a stamped
|
||||||
|
// collaborator receiver type for deeper hops.
|
||||||
if (!acceptVirtualHop(start, neighbor, selfAtStart)) {
|
if (!acceptVirtualHop(start, neighbor, selfAtStart)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String nextSelf = nextConcreteSelf(selfAtStart, start, neighbor);
|
String nextSelf = nextSelfAfterEdge(selfAtStart, start, edge);
|
||||||
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
||||||
List<String> path = new ArrayList<>(List.of(start, target));
|
List<String> path = new ArrayList<>(List.of(start, target));
|
||||||
result.add(path);
|
result.add(path);
|
||||||
@@ -279,6 +281,7 @@ public class CallGraphPathFinder {
|
|||||||
if (!acceptVirtualHop(start, neighbor, selfAtStart)) {
|
if (!acceptVirtualHop(start, neighbor, selfAtStart)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
String nextSelf = nextSelfAfterEdge(selfAtStart, start, edge);
|
||||||
|
|
||||||
String resolvedTarget = neighbor.equals(target) || isHeuristicMatch(neighbor, target) ? target : neighbor;
|
String resolvedTarget = neighbor.equals(target) || isHeuristicMatch(neighbor, target) ? target : neighbor;
|
||||||
List<String> hopPath = new ArrayList<>(List.of(start, resolvedTarget));
|
List<String> hopPath = new ArrayList<>(List.of(start, resolvedTarget));
|
||||||
@@ -288,7 +291,6 @@ public class CallGraphPathFinder {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String nextSelf = nextConcreteSelf(selfAtStart, start, neighbor);
|
|
||||||
if (resolvedTarget.equals(target)) {
|
if (resolvedTarget.equals(target)) {
|
||||||
result.add(hopPath);
|
result.add(hopPath);
|
||||||
} else {
|
} else {
|
||||||
@@ -375,12 +377,27 @@ public class CallGraphPathFinder {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefer a concrete {@link CallEdge#getReceiverTypeFqn()} stamped at graph construction
|
||||||
|
* (CHA-refined collaborator impl). Otherwise fall back to self-hierarchy narrowing.
|
||||||
|
*/
|
||||||
|
private String nextSelfAfterEdge(String concreteSelfFqn, String callerMethod, CallEdge edge) {
|
||||||
|
if (edge != null) {
|
||||||
|
String stamped = edge.getReceiverTypeFqn();
|
||||||
|
if (stamped != null && context.isConcreteClassType(stamped)) {
|
||||||
|
return stamped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String targetMethod = edge != null ? edge.getTargetMethod() : null;
|
||||||
|
return nextConcreteSelf(concreteSelfFqn, callerMethod, targetMethod);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reject CHA fan-out edges that dispatch to a sibling subclass incompatible with the entry's
|
* Reject CHA fan-out edges that dispatch to a sibling subclass incompatible with the entry's
|
||||||
* concrete runtime type — but only for self-hierarchy template dispatch
|
* concrete runtime type — but only for self-hierarchy template dispatch
|
||||||
* ({@code concreteSelf ≤ callerClass}). Collaborator abstract types
|
* ({@code concreteSelf ≤ callerClass}). Collaborator abstract types
|
||||||
* ({@code Controller → AbstractService → Impl}) must stay CHA-open; otherwise REST endpoints
|
* ({@code Controller → AbstractService → Impl}) must stay CHA-open unless a concrete
|
||||||
* lose every service-side path.
|
* {@code receiverTypeFqn} was adopted as the dispatch self on the incoming edge.
|
||||||
*/
|
*/
|
||||||
private boolean acceptVirtualHop(String callerMethod, String targetMethod, String concreteSelfFqn) {
|
private boolean acceptVirtualHop(String callerMethod, String targetMethod, String concreteSelfFqn) {
|
||||||
if (!isSelfHierarchyDispatch(callerMethod, concreteSelfFqn)) {
|
if (!isSelfHierarchyDispatch(callerMethod, concreteSelfFqn)) {
|
||||||
|
|||||||
@@ -246,8 +246,8 @@ class CallGraphPathFinderTest {
|
|||||||
context.scan(tempDir);
|
context.scan(tempDir);
|
||||||
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
|
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
|
||||||
|
|
||||||
// Controller entry self is NOT in AbstractSharedService hierarchy — pruning must fail open
|
// Controller entry self is NOT in AbstractSharedService hierarchy — without stamped
|
||||||
// so REST → shared abstract service still reaches impl hooks.
|
// receiver types, pruning must fail open so REST still reaches impl hooks.
|
||||||
Map<String, List<CallEdge>> graph = new HashMap<>();
|
Map<String, List<CallEdge>> graph = new HashMap<>();
|
||||||
graph.put("com.example.OrderController.pay", List.of(
|
graph.put("com.example.OrderController.pay", List.of(
|
||||||
new CallEdge("com.example.AbstractSharedService.process", List.of())));
|
new CallEdge("com.example.AbstractSharedService.process", List.of())));
|
||||||
@@ -273,4 +273,59 @@ class CallGraphPathFinderTest {
|
|||||||
.anyMatch(hop -> hop.contains("OrderSharedService.doWork"))
|
.anyMatch(hop -> hop.contains("OrderSharedService.doWork"))
|
||||||
.anyMatch(hop -> hop.contains("DocumentSharedService.doWork"));
|
.anyMatch(hop -> hop.contains("DocumentSharedService.doWork"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPruneCollaboratorTemplateFanOutWhenEdgeStampsConcreteReceiverType() throws Exception {
|
||||||
|
Path tempDir = Files.createTempDirectory("pathfinder_stamped_receiver");
|
||||||
|
Files.writeString(tempDir.resolve("Ops.java"), """
|
||||||
|
package com.example;
|
||||||
|
class OrderJmsListener {
|
||||||
|
void onMessage() { ops.process(); }
|
||||||
|
AbstractOps ops;
|
||||||
|
}
|
||||||
|
abstract class AbstractOps {
|
||||||
|
void process() { fire(); }
|
||||||
|
abstract void fire();
|
||||||
|
}
|
||||||
|
class PayOps extends AbstractOps {
|
||||||
|
void fire() {}
|
||||||
|
}
|
||||||
|
class ShipOps extends AbstractOps {
|
||||||
|
void fire() {}
|
||||||
|
}
|
||||||
|
class StateMachine { void sendEvent() {} }
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
|
||||||
|
|
||||||
|
Map<String, List<CallEdge>> graph = new HashMap<>();
|
||||||
|
graph.put("com.example.OrderJmsListener.onMessage", List.of(
|
||||||
|
new CallEdge(
|
||||||
|
"com.example.PayOps.process",
|
||||||
|
List.of(),
|
||||||
|
"ops",
|
||||||
|
null,
|
||||||
|
"com.example.PayOps")));
|
||||||
|
graph.put("com.example.AbstractOps.process", List.of(
|
||||||
|
new CallEdge("com.example.PayOps.fire", List.of()),
|
||||||
|
new CallEdge("com.example.ShipOps.fire", List.of())));
|
||||||
|
graph.put("com.example.PayOps.fire", List.of(
|
||||||
|
new CallEdge("com.example.StateMachine.sendEvent", List.of())));
|
||||||
|
graph.put("com.example.ShipOps.fire", List.of(
|
||||||
|
new CallEdge("com.example.StateMachine.sendEvent", List.of())));
|
||||||
|
|
||||||
|
List<List<String>> paths = pathFinder.findAllPaths(
|
||||||
|
"com.example.OrderJmsListener.onMessage",
|
||||||
|
"com.example.StateMachine.sendEvent",
|
||||||
|
graph,
|
||||||
|
new HashSet<>(),
|
||||||
|
"com.example.OrderJmsListener");
|
||||||
|
|
||||||
|
assertThat(paths).hasSize(1);
|
||||||
|
assertThat(paths.get(0))
|
||||||
|
.contains("com.example.PayOps.fire")
|
||||||
|
.doesNotContain("com.example.ShipOps.fire");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
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 org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JMS listeners often call a field typed as an abstract collaborator ({@code ops.process()}).
|
||||||
|
* CHA expands to each impl target and must stamp {@code receiverTypeFqn} so pathfinding can
|
||||||
|
* walk that impl's template body without sibling cross-wiring — and still produce edges.
|
||||||
|
*/
|
||||||
|
class JmsCollaboratorAbstractFieldCallGraphTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jmsListenerThroughAbstractOpsFieldReachesSendEvent(@TempDir Path tempDir) throws Exception {
|
||||||
|
writeFixture(tempDir);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
|
||||||
|
|
||||||
|
List<CallEdge> fromOnMessage = graph.get("com.example.OrderJmsListener.onMessage");
|
||||||
|
assertThat(fromOnMessage)
|
||||||
|
.extracting(CallEdge::getTargetMethod)
|
||||||
|
.as("ops.process() must CHA-expand to concrete collaborator impls")
|
||||||
|
.contains(
|
||||||
|
"com.example.PayOps.process",
|
||||||
|
"com.example.ShipOps.process");
|
||||||
|
assertThat(fromOnMessage)
|
||||||
|
.filteredOn(edge -> edge.getTargetMethod().contains("PayOps"))
|
||||||
|
.extracting(CallEdge::getReceiverTypeFqn)
|
||||||
|
.containsOnly("com.example.PayOps");
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(
|
||||||
|
List.of(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.JMS)
|
||||||
|
.name("JMS: orders")
|
||||||
|
.className("com.example.OrderJmsListener")
|
||||||
|
.methodName("onMessage")
|
||||||
|
.build()),
|
||||||
|
List.of(TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("sendEvent")
|
||||||
|
.event("event")
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
assertThat(chains)
|
||||||
|
.as("JMS → abstract ops field → fire → sendEvent must produce chains")
|
||||||
|
.isNotEmpty();
|
||||||
|
|
||||||
|
assertThat(chains)
|
||||||
|
.anyMatch(chain -> chain.getMethodChain().stream()
|
||||||
|
.anyMatch(hop -> hop.contains("PayOps")));
|
||||||
|
assertThat(chains)
|
||||||
|
.filteredOn(chain -> chain.getMethodChain().stream()
|
||||||
|
.anyMatch(hop -> hop.contains("PayOps")))
|
||||||
|
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||||
|
.noneMatch(hop -> hop.contains("ShipOps")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeFixture(Path tempDir) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("JmsOps.java"), """
|
||||||
|
package com.example;
|
||||||
|
|
||||||
|
class OrderJmsListener {
|
||||||
|
private AbstractOps ops;
|
||||||
|
void onMessage(String m) {
|
||||||
|
ops.process(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AbstractOps {
|
||||||
|
void process(String e) {
|
||||||
|
fire(e);
|
||||||
|
}
|
||||||
|
abstract void fire(String e);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PayOps extends AbstractOps {
|
||||||
|
private final StateMachine sm = new StateMachine();
|
||||||
|
@Override
|
||||||
|
void fire(String e) {
|
||||||
|
sm.sendEvent(OrderEvent.PAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShipOps extends AbstractOps {
|
||||||
|
private final StateMachine sm = new StateMachine();
|
||||||
|
@Override
|
||||||
|
void fire(String e) {
|
||||||
|
sm.sendEvent(OrderEvent.SHIP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMachine {
|
||||||
|
void sendEvent(Object event) {}
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY, SHIP }
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user