Fix map-routed dispatcher events to respect path-bound keys.
Resolve static map lookups along the call path, guard enum widening when keys are bound, and add isolation tests for warmed-cache map routing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import java.util.Set;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class ConstantResolver {
|
||||
@@ -19,6 +20,36 @@ public class ConstantResolver {
|
||||
return resolveInternal(expr, context, new HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves {@code map.get(key)} when the map receiver and key are statically known.
|
||||
*/
|
||||
public String resolveKeyedMapLookup(MethodInvocation getCall, CodebaseContext context) {
|
||||
if (getCall == null || getCall.getExpression() == null || getCall.arguments().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String methodName = getCall.getName().getIdentifier();
|
||||
if (!"get".equals(methodName) && !"getOrDefault".equals(methodName)) {
|
||||
return null;
|
||||
}
|
||||
return LiteralExpressionSupport.resolveMapGet(
|
||||
getCall, context, new HashSet<>(), (e, v) -> resolveInternal(e, context, v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a value from an encoded {@code MAP:...} literal or map-typed expression at {@code key}.
|
||||
*/
|
||||
public String resolveKeyedMapLookup(Expression mapExpr, String key, CodebaseContext context) {
|
||||
if (mapExpr == null || key == null) {
|
||||
return null;
|
||||
}
|
||||
String encoded = resolveInternal(mapExpr, context, new HashSet<>());
|
||||
if (encoded == null || !encoded.startsWith("MAP:")) {
|
||||
return null;
|
||||
}
|
||||
Map<String, String> entries = LiteralExpressionSupport.parseMapLiteral(encoded);
|
||||
return entries != null ? entries.get(key) : null;
|
||||
}
|
||||
|
||||
public static List<String> decodeMapLiteralValues(String encoded) {
|
||||
return LiteralExpressionSupport.mapLiteralValues(encoded);
|
||||
}
|
||||
@@ -818,12 +849,13 @@ public class ConstantResolver {
|
||||
}
|
||||
}
|
||||
|
||||
td = typeFromCurrentCallPath(context);
|
||||
if (td != null) {
|
||||
String fqn = context.getFqn(td);
|
||||
String result = resolveFieldInType(td, sn.getIdentifier(), fqn, context, visited);
|
||||
if (result != null) {
|
||||
return result;
|
||||
for (String classFqn : classesFromCurrentCallPath()) {
|
||||
TypeDeclaration pathTd = context.getTypeDeclaration(classFqn);
|
||||
if (pathTd != null) {
|
||||
String result = resolveFieldInType(pathTd, sn.getIdentifier(), classFqn, context, visited);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,21 +933,35 @@ public class ConstantResolver {
|
||||
}
|
||||
|
||||
private TypeDeclaration typeFromCurrentCallPath(CodebaseContext context) {
|
||||
String entryClass = getCurrentCallPathEntryClass();
|
||||
if (entryClass == null) {
|
||||
List<String> classes = classesFromCurrentCallPath();
|
||||
if (classes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return context.getTypeDeclaration(entryClass);
|
||||
return context.getTypeDeclaration(classes.get(0));
|
||||
}
|
||||
|
||||
private List<String> classesFromCurrentCallPath() {
|
||||
List<String> path = JdtDataFlowModel.getCurrentPath();
|
||||
if (path == null || path.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> classes = new ArrayList<>();
|
||||
for (int i = path.size() - 1; i >= 0; i--) {
|
||||
String methodFqn = path.get(i);
|
||||
int dot = methodFqn.lastIndexOf('.');
|
||||
if (dot > 0) {
|
||||
String className = methodFqn.substring(0, dot);
|
||||
if (!classes.contains(className)) {
|
||||
classes.add(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
private String getCurrentCallPathEntryClass() {
|
||||
List<String> path = JdtDataFlowModel.getCurrentPath();
|
||||
if (path == null || path.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String entryMethod = path.get(0);
|
||||
int dot = entryMethod.lastIndexOf('.');
|
||||
return dot > 0 ? entryMethod.substring(0, dot) : null;
|
||||
List<String> classes = classesFromCurrentCallPath();
|
||||
return classes.isEmpty() ? null : classes.get(classes.size() - 1);
|
||||
}
|
||||
|
||||
private String resolveFieldInType(TypeDeclaration td, String fieldName, String typeFqn, CodebaseContext context, Set<String> visited) {
|
||||
|
||||
@@ -428,6 +428,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
while (exprNode instanceof ParenthesizedExpression pe) {
|
||||
exprNode = pe.getExpression();
|
||||
}
|
||||
boolean[] keyedMapLookupOnInitializer = {false};
|
||||
boolean[] pathBoundMapKeyOnInitializer = {false};
|
||||
|
||||
if (exprNode instanceof Expression expressionNode) {
|
||||
String remappedGetter = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||
@@ -461,8 +463,25 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if ("get".equals(methodName) || "getOrDefault".equals(methodName)) {
|
||||
boolean keyedLookup = expressionAccessClassifier.isKeyedLookup(mi, entryMethod);
|
||||
boolean functionalGet = "get".equals(methodName) && mi.arguments().isEmpty() && !keyedLookup;
|
||||
if (keyedLookup || functionalGet) {
|
||||
constantExtractor.extractConstantsFromExpression((Expression) exprNode, polymorphicEvents);
|
||||
if (keyedLookup) {
|
||||
List<String> keyedResults = resolveKeyedMapLookup(mi, path, callGraph);
|
||||
if (!keyedResults.isEmpty()) {
|
||||
polymorphicEvents.addAll(keyedResults);
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
|
||||
}
|
||||
boolean pathBoundKey = hasPathBindingForMapKey(mi, path, callGraph);
|
||||
if (pathBoundKey) {
|
||||
keyedMapLookupOnInitializer[0] = true;
|
||||
pathBoundMapKeyOnInitializer[0] = true;
|
||||
}
|
||||
if (!pathBoundKey) {
|
||||
addConstantsFromTracedExpression((Expression) exprNode, polymorphicEvents, path, callGraph, entryMethod);
|
||||
if (!polymorphicEvents.isEmpty()) {
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
|
||||
}
|
||||
}
|
||||
} else if (functionalGet) {
|
||||
addConstantsFromTracedExpression((Expression) exprNode, polymorphicEvents, path, callGraph, entryMethod);
|
||||
if (!polymorphicEvents.isEmpty()) {
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
@@ -615,6 +634,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
branches.add(cond.getThenExpression());
|
||||
branches.add(cond.getElseExpression());
|
||||
for (Expression branch : branches) {
|
||||
if (branch instanceof ConditionalExpression nestedCond) {
|
||||
// traceVariableAll collapses literal "true"/"false" conditions to a single branch;
|
||||
// extract recursively so chained if-else assignments keep all enum variants.
|
||||
constantExtractor.extractConstantsFromExpression(nestedCond, polymorphicEvents);
|
||||
continue;
|
||||
}
|
||||
List<Expression> tracedBranch = variableTracer.traceVariableAll(branch);
|
||||
for (Expression tb : tracedBranch) {
|
||||
if (tb instanceof ClassInstanceCreation cic) {
|
||||
@@ -764,11 +789,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1];
|
||||
if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) {
|
||||
ASTNode newNode = parseExpressionString(resolvedValue);
|
||||
if (newNode instanceof Expression) {
|
||||
List<Expression> traced = variableTracer.traceVariableAll((Expression) newNode);
|
||||
for (Expression ex : traced) {
|
||||
constantExtractor.extractConstantsFromExpression(ex, polymorphicEvents);
|
||||
}
|
||||
if (newNode instanceof Expression newExpr) {
|
||||
trackKeyedMapLookupFlags(newExpr, path, callGraph, entryMethod, keyedMapLookupOnInitializer, pathBoundMapKeyOnInitializer);
|
||||
addConstantsFromTracedExpression(newExpr, polymorphicEvents, path, callGraph, entryMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -790,11 +813,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
String initializer = variableTracer.traceLocalVariable(methodFqn, varName);
|
||||
if (initializer != null && !initializer.equals(varName)) {
|
||||
ASTNode mapNode = parseExpressionString(initializer);
|
||||
if (mapNode instanceof Expression) {
|
||||
List<Expression> mapTraced = variableTracer.traceVariableAll((Expression) mapNode);
|
||||
for (Expression t : mapTraced) {
|
||||
constantExtractor.extractConstantsFromExpression(t, polymorphicEvents);
|
||||
}
|
||||
if (mapNode instanceof Expression initExpr) {
|
||||
trackKeyedMapLookupFlags(initExpr, path, callGraph, methodFqn, keyedMapLookupOnInitializer, pathBoundMapKeyOnInitializer);
|
||||
addConstantsFromTracedExpression(initExpr, polymorphicEvents, path, callGraph, methodFqn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -836,7 +857,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (enumValues != null && !enumValues.isEmpty()) {
|
||||
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
||||
&& !tp.isExternal()
|
||||
&& !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)) {
|
||||
&& !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)
|
||||
&& !(keyedMapLookupOnInitializer[0] && pathBoundMapKeyOnInitializer[0])) {
|
||||
for (String ev : enumValues) {
|
||||
polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev));
|
||||
}
|
||||
@@ -885,8 +907,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidConstant && exprNode instanceof Expression) {
|
||||
constantExtractor.extractConstantsFromExpression((Expression) exprNode, polymorphicEvents);
|
||||
if (!hasValidConstant && exprNode instanceof Expression expressionFallback) {
|
||||
addConstantsFromTracedExpression(expressionFallback, polymorphicEvents, path, callGraph, entryMethod);
|
||||
}
|
||||
|
||||
List<String> newPolyEvents = new ArrayList<>();
|
||||
@@ -964,6 +986,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
|
||||
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
|
||||
|
||||
polymorphicEvents = narrowPolymorphicEventsWithPathBindings(polymorphicEvents, path, callGraph, tp);
|
||||
|
||||
Expression runtimeExpr = exprNode instanceof Expression expression ? expression : null;
|
||||
if (isRuntimeEnumParameter(runtimeExpr) || tp.isExternal()) {
|
||||
polymorphicEvents.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
||||
@@ -2047,4 +2071,187 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
return mi.arguments().get(0) instanceof SimpleName;
|
||||
}
|
||||
|
||||
private List<String> resolveKeyedMapLookup(
|
||||
MethodInvocation getCall, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
List<String> results = new ArrayList<>();
|
||||
String literalResult = constantResolver.resolveKeyedMapLookup(getCall, context);
|
||||
if (literalResult != null && !literalResult.isBlank()) {
|
||||
addResolvedConstantToList(literalResult, results);
|
||||
return results;
|
||||
}
|
||||
if (getCall.arguments().isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
Expression keyArg = (Expression) getCall.arguments().get(0);
|
||||
String resolvedKey = resolveStringArgumentFromPath(keyArg, path, callGraph);
|
||||
if (resolvedKey != null && getCall.getExpression() != null) {
|
||||
String mapValue = resolveKeyedValueFromMapExpression(getCall.getExpression(), resolvedKey);
|
||||
if (mapValue != null) {
|
||||
addResolvedConstantToList(mapValue, results);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private String resolveStringArgumentFromPath(
|
||||
Expression keyArg, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
if (keyArg instanceof StringLiteral sl) {
|
||||
return sl.getLiteralValue();
|
||||
}
|
||||
if (keyArg instanceof SimpleName sn) {
|
||||
Map<String, String> bindings = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
|
||||
String bound = bindings.get(sn.getIdentifier());
|
||||
if (bound != null) {
|
||||
return unwrapStringLiteral(bound);
|
||||
}
|
||||
for (int i = path.size() - 1; i >= 0; i--) {
|
||||
String traced = variableTracer.traceLocalVariable(path.get(i), sn.getIdentifier());
|
||||
if (traced != null && !traced.equals(sn.getIdentifier())) {
|
||||
String unwrapped = unwrapStringLiteral(traced);
|
||||
if (unwrapped != null && !unwrapped.isBlank()) {
|
||||
return unwrapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String resolved = constantResolver.resolve(keyArg, context);
|
||||
return unwrapStringLiteral(resolved);
|
||||
}
|
||||
|
||||
private boolean hasPathBindingForMapKey(
|
||||
MethodInvocation getCall, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
if (getCall.arguments().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Expression keyArg = (Expression) getCall.arguments().get(0);
|
||||
if (!(keyArg instanceof SimpleName sn)) {
|
||||
return false;
|
||||
}
|
||||
Map<String, String> bindings = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
|
||||
if (bindings.containsKey(sn.getIdentifier())) {
|
||||
return true;
|
||||
}
|
||||
for (String methodFqn : path) {
|
||||
String traced = variableTracer.traceLocalVariable(methodFqn, sn.getIdentifier());
|
||||
if (traced != null && !traced.equals(sn.getIdentifier()) && unwrapStringLiteral(traced) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void addConstantsFromTracedExpression(
|
||||
Expression expr, List<String> constants, List<String> path, Map<String, List<CallEdge>> callGraph, String scopeMethod) {
|
||||
List<Expression> traced = variableTracer.traceVariableAll(expr);
|
||||
for (Expression tracedExpr : traced) {
|
||||
addConstantsFromSingleExpression(tracedExpr, constants, path, callGraph, scopeMethod);
|
||||
}
|
||||
if (constants.isEmpty()) {
|
||||
addConstantsFromSingleExpression(expr, constants, path, callGraph, scopeMethod);
|
||||
}
|
||||
}
|
||||
|
||||
private void addConstantsFromSingleExpression(
|
||||
Expression expr, List<String> constants, List<String> path, Map<String, List<CallEdge>> callGraph, String scopeMethod) {
|
||||
if (expr instanceof MethodInvocation mi && expressionAccessClassifier.isKeyedLookup(mi, scopeMethod)) {
|
||||
List<String> keyed = resolveKeyedMapLookup(mi, path, callGraph);
|
||||
if (!keyed.isEmpty()) {
|
||||
for (String keyedValue : keyed) {
|
||||
if (!constants.contains(keyedValue)) {
|
||||
constants.add(keyedValue);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!hasPathBindingForMapKey(mi, path, callGraph)) {
|
||||
constantExtractor.extractConstantsFromExpression(expr, constants);
|
||||
}
|
||||
return;
|
||||
}
|
||||
constantExtractor.extractConstantsFromExpression(expr, constants);
|
||||
}
|
||||
|
||||
private List<String> narrowPolymorphicEventsWithPathBindings(
|
||||
List<String> polymorphicEvents,
|
||||
List<String> path,
|
||||
Map<String, List<CallEdge>> callGraph,
|
||||
TriggerPoint tp) {
|
||||
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
|
||||
return polymorphicEvents;
|
||||
}
|
||||
Map<String, String> bindings = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
|
||||
if (bindings.isEmpty()) {
|
||||
return polymorphicEvents;
|
||||
}
|
||||
List<String> narrowed = new ArrayList<>(polymorphicEvents);
|
||||
boolean hasConcrete = narrowed.stream().anyMatch(this::looksLikeEnumConstant);
|
||||
if (hasConcrete) {
|
||||
narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
||||
}
|
||||
if (tp.isExternal()) {
|
||||
narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
||||
}
|
||||
return narrowed;
|
||||
}
|
||||
|
||||
private String resolveKeyedValueFromMapExpression(Expression mapExpr, String key) {
|
||||
if (mapExpr == null || key == null) {
|
||||
return null;
|
||||
}
|
||||
return constantResolver.resolveKeyedMapLookup(mapExpr, key, context);
|
||||
}
|
||||
|
||||
private static String unwrapStringLiteral(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length() >= 2) {
|
||||
return trimmed.substring(1, trimmed.length() - 1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private boolean looksLikeEnumConstant(String value) {
|
||||
if (value == null || value.isBlank() || value.startsWith("<SYMBOLIC:")) {
|
||||
return false;
|
||||
}
|
||||
String name = value.contains(".") ? value.substring(value.lastIndexOf('.') + 1) : value;
|
||||
return name.matches("[A-Z_][A-Z0-9_]*");
|
||||
}
|
||||
|
||||
private void trackKeyedMapLookupFlags(
|
||||
Expression expr,
|
||||
List<String> path,
|
||||
Map<String, List<CallEdge>> callGraph,
|
||||
String scopeMethod,
|
||||
boolean[] keyedMapLookupOnInitializer,
|
||||
boolean[] pathBoundMapKeyOnInitializer) {
|
||||
if (expr instanceof MethodInvocation mi && expressionAccessClassifier.isKeyedLookup(mi, scopeMethod)) {
|
||||
keyedMapLookupOnInitializer[0] = true;
|
||||
if (hasPathBindingForMapKey(mi, path, callGraph)) {
|
||||
pathBoundMapKeyOnInitializer[0] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addResolvedConstantToList(String resolved, List<String> results) {
|
||||
if (resolved == null || resolved.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (resolved.startsWith("ENUM_SET:")) {
|
||||
for (String part : resolved.substring(9).split(",")) {
|
||||
String parsed = constantExtractor.parseEnumSetElement(part);
|
||||
if (!results.contains(parsed)) {
|
||||
results.add(parsed);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
String parsed = constantExtractor.parseEnumSetElement(resolved);
|
||||
if (!results.contains(parsed)) {
|
||||
results.add(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,11 @@ public final class ExpressionAccessClassifier {
|
||||
return "Map";
|
||||
}
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
return lookupDeclaredType(scopeMethod, sn.getIdentifier());
|
||||
String declared = lookupDeclaredType(scopeMethod, sn.getIdentifier());
|
||||
if (declared != null) {
|
||||
return declared;
|
||||
}
|
||||
return lookupFieldType(classNameFromScopeMethod(scopeMethod), sn.getIdentifier());
|
||||
}
|
||||
if (receiver instanceof ThisExpression) {
|
||||
return classNameFromScopeMethod(scopeMethod);
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
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 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;
|
||||
|
||||
/**
|
||||
* Guards map-routed endpoint resolution and per-machine polymorphic event isolation when the
|
||||
* call graph and accessor caches are reused across multiple {@link HeuristicCallGraphEngine#findChains}
|
||||
* invocations on the same {@link CodebaseContext}.
|
||||
*/
|
||||
class PerformanceCacheMultiMachineIsolationTest {
|
||||
|
||||
private static final String ORDER_CONTROLLER = "com.example.OrderController";
|
||||
private static final String SHIPMENT_CONTROLLER = "com.example.ShipmentController";
|
||||
private static final String ORDER_MACHINE = "com.example.OrderMachine";
|
||||
private static final String SHIPMENT_MACHINE = "com.example.ShipmentMachine";
|
||||
|
||||
@Test
|
||||
void mapRoutedEndpointsShouldResolveSingleEventPerLiteralEndpoint(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.Map;
|
||||
public class OrderController {
|
||||
OrderMachine sm;
|
||||
public void payOrder() { sm.sendEvent(Routes.ROUTES.get("order.pay")); }
|
||||
public void shipOrder() { sm.sendEvent(Routes.ROUTES.get("order.ship")); }
|
||||
}
|
||||
class Routes {
|
||||
static final Map<String, OrderEvent> ROUTES = Map.of(
|
||||
"order.pay", OrderEvent.PAY,
|
||||
"order.ship", OrderEvent.SHIP
|
||||
);
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP, CANCEL }
|
||||
class OrderMachine { public void sendEvent(OrderEvent e) {} }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("App.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className(ORDER_MACHINE)
|
||||
.methodName("sendEvent")
|
||||
.event("e")
|
||||
.build();
|
||||
|
||||
EntryPoint payEntry = EntryPoint.builder().className(ORDER_CONTROLLER).methodName("payOrder").build();
|
||||
EntryPoint shipEntry = EntryPoint.builder().className(ORDER_CONTROLLER).methodName("shipOrder").build();
|
||||
|
||||
CallChain payChain = engine.findChains(List.of(payEntry), List.of(trigger)).get(0);
|
||||
CallChain shipChain = engine.findChains(List.of(shipEntry), List.of(trigger)).get(0);
|
||||
|
||||
assertThat(payChain.getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderEvent.PAY");
|
||||
assertThat(shipChain.getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderEvent.SHIP");
|
||||
|
||||
assertThat(context.getCache()).containsKey("heuristicCallGraph");
|
||||
|
||||
CallChain payAgain = engine.findChains(List.of(payEntry), List.of(trigger)).get(0);
|
||||
CallChain shipAgain = engine.findChains(List.of(shipEntry), List.of(trigger)).get(0);
|
||||
|
||||
assertThat(payAgain.getTriggerPoint().getPolymorphicEvents())
|
||||
.isEqualTo(payChain.getTriggerPoint().getPolymorphicEvents());
|
||||
assertThat(shipAgain.getTriggerPoint().getPolymorphicEvents())
|
||||
.isEqualTo(shipChain.getTriggerPoint().getPolymorphicEvents());
|
||||
}
|
||||
|
||||
@Test
|
||||
void separateMachinesShouldNotCrossContaminateMapRoutedResolution(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
import java.util.Map;
|
||||
public class OrderController {
|
||||
OrderMachine orderMachine;
|
||||
public void pay() { orderMachine.sendEvent(OrderRoutes.ROUTES.get("order.pay")); }
|
||||
}
|
||||
public class ShipmentController {
|
||||
ShipmentMachine shipmentMachine;
|
||||
public void dispatch() { shipmentMachine.sendEvent(ShipmentRoutes.ROUTES.get("shipment.dispatch")); }
|
||||
}
|
||||
class OrderRoutes {
|
||||
static final Map<String, OrderEvent> ROUTES = Map.of("order.pay", OrderEvent.PAY);
|
||||
}
|
||||
class ShipmentRoutes {
|
||||
static final Map<String, ShipmentEvent> ROUTES = Map.of("shipment.dispatch", ShipmentEvent.DISPATCH);
|
||||
}
|
||||
enum OrderEvent { PAY }
|
||||
enum ShipmentEvent { DISPATCH }
|
||||
class OrderMachine { public void sendEvent(OrderEvent e) {} }
|
||||
class ShipmentMachine { public void sendEvent(ShipmentEvent e) {} }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("App.java"), source);
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint orderEntry = EntryPoint.builder().className(ORDER_CONTROLLER).methodName("pay").build();
|
||||
EntryPoint shipmentEntry = EntryPoint.builder().className(SHIPMENT_CONTROLLER).methodName("dispatch").build();
|
||||
TriggerPoint orderTrigger = TriggerPoint.builder()
|
||||
.className(ORDER_MACHINE)
|
||||
.methodName("sendEvent")
|
||||
.event("e")
|
||||
.build();
|
||||
TriggerPoint shipmentTrigger = TriggerPoint.builder()
|
||||
.className(SHIPMENT_MACHINE)
|
||||
.methodName("sendEvent")
|
||||
.event("e")
|
||||
.build();
|
||||
|
||||
engine.findChains(List.of(orderEntry), List.of(orderTrigger));
|
||||
CallChain shipmentChain = engine.findChains(List.of(shipmentEntry), List.of(shipmentTrigger)).get(0);
|
||||
assertThat(shipmentChain.getTriggerPoint().getPolymorphicEvents()).containsExactly("ShipmentEvent.DISPATCH");
|
||||
|
||||
CallChain orderChain = engine.findChains(List.of(orderEntry), List.of(orderTrigger)).get(0);
|
||||
assertThat(orderChain.getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderEvent.PAY");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user