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.LinkedHashSet;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ConstantResolver {
|
public class ConstantResolver {
|
||||||
@@ -19,6 +20,36 @@ public class ConstantResolver {
|
|||||||
return resolveInternal(expr, context, new HashSet<>());
|
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) {
|
public static List<String> decodeMapLiteralValues(String encoded) {
|
||||||
return LiteralExpressionSupport.mapLiteralValues(encoded);
|
return LiteralExpressionSupport.mapLiteralValues(encoded);
|
||||||
}
|
}
|
||||||
@@ -818,14 +849,15 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td = typeFromCurrentCallPath(context);
|
for (String classFqn : classesFromCurrentCallPath()) {
|
||||||
if (td != null) {
|
TypeDeclaration pathTd = context.getTypeDeclaration(classFqn);
|
||||||
String fqn = context.getFqn(td);
|
if (pathTd != null) {
|
||||||
String result = resolveFieldInType(td, sn.getIdentifier(), fqn, context, visited);
|
String result = resolveFieldInType(pathTd, sn.getIdentifier(), classFqn, context, visited);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: Static imports
|
// Fallback: Static imports
|
||||||
ASTNode root = sn.getRoot();
|
ASTNode root = sn.getRoot();
|
||||||
@@ -901,21 +933,35 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private TypeDeclaration typeFromCurrentCallPath(CodebaseContext context) {
|
private TypeDeclaration typeFromCurrentCallPath(CodebaseContext context) {
|
||||||
String entryClass = getCurrentCallPathEntryClass();
|
List<String> classes = classesFromCurrentCallPath();
|
||||||
if (entryClass == null) {
|
if (classes.isEmpty()) {
|
||||||
return null;
|
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() {
|
private String getCurrentCallPathEntryClass() {
|
||||||
List<String> path = JdtDataFlowModel.getCurrentPath();
|
List<String> classes = classesFromCurrentCallPath();
|
||||||
if (path == null || path.isEmpty()) {
|
return classes.isEmpty() ? null : classes.get(classes.size() - 1);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String entryMethod = path.get(0);
|
|
||||||
int dot = entryMethod.lastIndexOf('.');
|
|
||||||
return dot > 0 ? entryMethod.substring(0, dot) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveFieldInType(TypeDeclaration td, String fieldName, String typeFqn, CodebaseContext context, Set<String> visited) {
|
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) {
|
while (exprNode instanceof ParenthesizedExpression pe) {
|
||||||
exprNode = pe.getExpression();
|
exprNode = pe.getExpression();
|
||||||
}
|
}
|
||||||
|
boolean[] keyedMapLookupOnInitializer = {false};
|
||||||
|
boolean[] pathBoundMapKeyOnInitializer = {false};
|
||||||
|
|
||||||
if (exprNode instanceof Expression expressionNode) {
|
if (exprNode instanceof Expression expressionNode) {
|
||||||
String remappedGetter = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
String remappedGetter = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||||
@@ -461,8 +463,25 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if ("get".equals(methodName) || "getOrDefault".equals(methodName)) {
|
if ("get".equals(methodName) || "getOrDefault".equals(methodName)) {
|
||||||
boolean keyedLookup = expressionAccessClassifier.isKeyedLookup(mi, entryMethod);
|
boolean keyedLookup = expressionAccessClassifier.isKeyedLookup(mi, entryMethod);
|
||||||
boolean functionalGet = "get".equals(methodName) && mi.arguments().isEmpty() && !keyedLookup;
|
boolean functionalGet = "get".equals(methodName) && mi.arguments().isEmpty() && !keyedLookup;
|
||||||
if (keyedLookup || functionalGet) {
|
if (keyedLookup) {
|
||||||
constantExtractor.extractConstantsFromExpression((Expression) exprNode, polymorphicEvents);
|
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()) {
|
if (!polymorphicEvents.isEmpty()) {
|
||||||
return TriggerPoint.builder()
|
return TriggerPoint.builder()
|
||||||
.event(resolvedValue)
|
.event(resolvedValue)
|
||||||
@@ -615,6 +634,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
branches.add(cond.getThenExpression());
|
branches.add(cond.getThenExpression());
|
||||||
branches.add(cond.getElseExpression());
|
branches.add(cond.getElseExpression());
|
||||||
for (Expression branch : branches) {
|
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);
|
List<Expression> tracedBranch = variableTracer.traceVariableAll(branch);
|
||||||
for (Expression tb : tracedBranch) {
|
for (Expression tb : tracedBranch) {
|
||||||
if (tb instanceof ClassInstanceCreation cic) {
|
if (tb instanceof ClassInstanceCreation cic) {
|
||||||
@@ -764,11 +789,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1];
|
resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1];
|
||||||
if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) {
|
if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) {
|
||||||
ASTNode newNode = parseExpressionString(resolvedValue);
|
ASTNode newNode = parseExpressionString(resolvedValue);
|
||||||
if (newNode instanceof Expression) {
|
if (newNode instanceof Expression newExpr) {
|
||||||
List<Expression> traced = variableTracer.traceVariableAll((Expression) newNode);
|
trackKeyedMapLookupFlags(newExpr, path, callGraph, entryMethod, keyedMapLookupOnInitializer, pathBoundMapKeyOnInitializer);
|
||||||
for (Expression ex : traced) {
|
addConstantsFromTracedExpression(newExpr, polymorphicEvents, path, callGraph, entryMethod);
|
||||||
constantExtractor.extractConstantsFromExpression(ex, polymorphicEvents);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -790,11 +813,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
String initializer = variableTracer.traceLocalVariable(methodFqn, varName);
|
String initializer = variableTracer.traceLocalVariable(methodFqn, varName);
|
||||||
if (initializer != null && !initializer.equals(varName)) {
|
if (initializer != null && !initializer.equals(varName)) {
|
||||||
ASTNode mapNode = parseExpressionString(initializer);
|
ASTNode mapNode = parseExpressionString(initializer);
|
||||||
if (mapNode instanceof Expression) {
|
if (mapNode instanceof Expression initExpr) {
|
||||||
List<Expression> mapTraced = variableTracer.traceVariableAll((Expression) mapNode);
|
trackKeyedMapLookupFlags(initExpr, path, callGraph, methodFqn, keyedMapLookupOnInitializer, pathBoundMapKeyOnInitializer);
|
||||||
for (Expression t : mapTraced) {
|
addConstantsFromTracedExpression(initExpr, polymorphicEvents, path, callGraph, methodFqn);
|
||||||
constantExtractor.extractConstantsFromExpression(t, polymorphicEvents);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -836,7 +857,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (enumValues != null && !enumValues.isEmpty()) {
|
if (enumValues != null && !enumValues.isEmpty()) {
|
||||||
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
||||||
&& !tp.isExternal()
|
&& !tp.isExternal()
|
||||||
&& !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)) {
|
&& !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)
|
||||||
|
&& !(keyedMapLookupOnInitializer[0] && pathBoundMapKeyOnInitializer[0])) {
|
||||||
for (String ev : enumValues) {
|
for (String ev : enumValues) {
|
||||||
polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev));
|
polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev));
|
||||||
}
|
}
|
||||||
@@ -885,8 +907,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasValidConstant && exprNode instanceof Expression) {
|
if (!hasValidConstant && exprNode instanceof Expression expressionFallback) {
|
||||||
constantExtractor.extractConstantsFromExpression((Expression) exprNode, polymorphicEvents);
|
addConstantsFromTracedExpression(expressionFallback, polymorphicEvents, path, callGraph, entryMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> newPolyEvents = new ArrayList<>();
|
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 = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
|
polymorphicEvents = narrowPolymorphicEventsWithPathBindings(polymorphicEvents, path, callGraph, tp);
|
||||||
|
|
||||||
Expression runtimeExpr = exprNode instanceof Expression expression ? expression : null;
|
Expression runtimeExpr = exprNode instanceof Expression expression ? expression : null;
|
||||||
if (isRuntimeEnumParameter(runtimeExpr) || tp.isExternal()) {
|
if (isRuntimeEnumParameter(runtimeExpr) || tp.isExternal()) {
|
||||||
polymorphicEvents.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
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;
|
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";
|
return "Map";
|
||||||
}
|
}
|
||||||
if (receiver instanceof SimpleName sn) {
|
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) {
|
if (receiver instanceof ThisExpression) {
|
||||||
return classNameFromScopeMethod(scopeMethod);
|
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