Prune switch-dispatch call paths and scope analysis per state machine.
Switch-case constraints on call-graph edges and forward path binding stop string-key endpoints from linking every dispatch arm; machine-scoped triggers/call chains reduce cross-config noise, with pipeline bug fixes and regression coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,8 +18,9 @@ public class CallChainEnricher implements AnalysisEnricher {
|
||||
|
||||
List<CallChain> chains = intelligence.findCallChains(
|
||||
result.getMetadata().getEntryPoints(),
|
||||
result.getMetadata().getTriggers()
|
||||
intelligence.findTriggerPoints()
|
||||
);
|
||||
chains = MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
||||
|
||||
result.addMetadata(CodebaseMetadata.builder()
|
||||
.callChains(chains)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.BeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Filters codebase-wide analysis artifacts to those relevant for a single state machine config.
|
||||
*/
|
||||
public final class MachineScopeFilter {
|
||||
|
||||
private static final BeanResolutionEngine ROUTING = new HeuristicBeanResolutionEngine();
|
||||
|
||||
private MachineScopeFilter() {
|
||||
}
|
||||
|
||||
public static List<TriggerPoint> filterTriggersForMachine(
|
||||
List<TriggerPoint> triggers, String machineName, CodebaseContext context) {
|
||||
if (triggers == null || machineName == null) {
|
||||
return triggers == null ? List.of() : triggers;
|
||||
}
|
||||
List<TriggerPoint> filtered = new ArrayList<>();
|
||||
for (TriggerPoint trigger : triggers) {
|
||||
if (isTriggerForMachine(trigger, machineName, context)) {
|
||||
filtered.add(trigger);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public static List<CallChain> filterCallChainsForMachine(
|
||||
List<CallChain> chains, String machineName, CodebaseContext context) {
|
||||
if (chains == null || machineName == null) {
|
||||
return chains == null ? List.of() : chains;
|
||||
}
|
||||
List<CallChain> filtered = new ArrayList<>();
|
||||
for (CallChain chain : chains) {
|
||||
if (ROUTING.isRoutedToCorrectMachine(chain, machineName, context)) {
|
||||
filtered.add(chain);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static boolean isTriggerForMachine(TriggerPoint trigger, String machineName, CodebaseContext context) {
|
||||
if (trigger == null) {
|
||||
return false;
|
||||
}
|
||||
CallChain probe = CallChain.builder().triggerPoint(trigger).methodChain(List.of()).build();
|
||||
return ROUTING.isRoutedToCorrectMachine(probe, machineName, context);
|
||||
}
|
||||
}
|
||||
@@ -93,9 +93,11 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the metadata with the new call chains
|
||||
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
|
||||
result.getMetadata().getTriggers(), result.getName(), context);
|
||||
|
||||
CodebaseMetadata updatedMetadata = CodebaseMetadata.builder()
|
||||
.triggers(result.getMetadata().getTriggers())
|
||||
.triggers(scopedTriggers)
|
||||
.entryPoints(result.getMetadata().getEntryPoints())
|
||||
.callChains(updatedChains)
|
||||
.properties(result.getMetadata().getProperties())
|
||||
|
||||
@@ -16,9 +16,8 @@ public class TriggerEnricher implements AnalysisEnricher {
|
||||
log.info("Enriching {} with triggers", result.getName());
|
||||
|
||||
List<TriggerPoint> triggers = intelligence.findTriggerPoints();
|
||||
|
||||
// Initially, we just add all triggers found in the codebase to the metadata.
|
||||
// In later steps of Phase 2, we will filter them by State Machine ID.
|
||||
triggers = MachineScopeFilter.filterTriggersForMachine(triggers, result.getName(), context);
|
||||
|
||||
result.addMetadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
|
||||
.triggers(triggers)
|
||||
.build());
|
||||
|
||||
@@ -219,6 +219,18 @@ public final class AccessorResolver {
|
||||
if (!cicValues.isEmpty()) {
|
||||
return cicValues;
|
||||
}
|
||||
} else if (receiverContext != null && receiverContext.cic() != null) {
|
||||
ReceiverContext enriched = new ReceiverContext(
|
||||
context.getTypeDeclaration(ownerFqn),
|
||||
receiverContext.cic(),
|
||||
receiverContext.contextCu(),
|
||||
receiverContext.anonymousGetter(),
|
||||
receiverContext.fieldValues());
|
||||
List<String> cicValues = resolveWithCic(
|
||||
ownerFqn, methodName, enriched, indexedAccessor.orElse(null), visited);
|
||||
if (!cicValues.isEmpty()) {
|
||||
return cicValues;
|
||||
}
|
||||
}
|
||||
|
||||
if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
|
||||
@@ -258,7 +270,6 @@ public final class AccessorResolver {
|
||||
}
|
||||
|
||||
if (receiverContext != null
|
||||
&& receiverContext.cic() != null
|
||||
&& receiverContext.fieldValues() != null
|
||||
&& accessor.fieldName() != null) {
|
||||
String fieldValue = receiverContext.fieldValues().get(accessor.fieldName());
|
||||
@@ -276,6 +287,9 @@ public final class AccessorResolver {
|
||||
AccessorSummary indexedAccessor,
|
||||
Set<String> visited) {
|
||||
TypeDeclaration td = receiverContext.ownerType();
|
||||
if (td == null) {
|
||||
td = context.getTypeDeclaration(ownerFqn);
|
||||
}
|
||||
ClassInstanceCreation cic = receiverContext.cic();
|
||||
Map<String, String> fieldValues = receiverContext.fieldValues() != null
|
||||
? receiverContext.fieldValues()
|
||||
@@ -400,7 +414,10 @@ public final class AccessorResolver {
|
||||
String cicKey = receiverContext != null && receiverContext.cic() != null
|
||||
? receiverContext.cic().toString()
|
||||
: "";
|
||||
return ownerFqn + "#" + methodName + "#" + cicKey;
|
||||
String fieldValuesKey = receiverContext != null && receiverContext.fieldValues() != null
|
||||
? receiverContext.fieldValues().toString()
|
||||
: "";
|
||||
return ownerFqn + "#" + methodName + "#" + cicKey + "#" + fieldValuesKey;
|
||||
}
|
||||
|
||||
private static String simplifyTypeName(String typeFqn) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -60,6 +62,66 @@ public final class BooleanConstraintEvaluator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a constraint against known variable bindings gathered while walking a call path forward.
|
||||
* Supports {@code var == VALUE} where VALUE may be a quoted string or enum constant; suffix matching
|
||||
* is used so {@code command == ORDER_PAY} matches {@code DomainCommand.ORDER_PAY}.
|
||||
*/
|
||||
public static boolean isCompatibleWithBindings(String constraint, Map<String, String> bindings) {
|
||||
if (constraint == null || constraint.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (String varName : extractEqualityVariables(constraint)) {
|
||||
if (!bindings.containsKey(varName) || varName.equals(bindings.get(varName))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
String expr = constraint;
|
||||
for (Map.Entry<String, String> entry : bindings.entrySet()) {
|
||||
expr = substituteVariableBindings(expr, entry.getKey(), entry.getValue());
|
||||
}
|
||||
return evaluateBooleanExpression(expr);
|
||||
}
|
||||
|
||||
private static Set<String> extractEqualityVariables(String constraint) {
|
||||
Set<String> vars = new HashSet<>();
|
||||
Pattern pattern = Pattern.compile("([a-zA-Z][\\w]*)\\s*==");
|
||||
Matcher matcher = pattern.matcher(constraint);
|
||||
while (matcher.find()) {
|
||||
vars.add(matcher.group(1));
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
private static String substituteVariableBindings(String expr, String varName, String boundValue) {
|
||||
if (boundValue == null || boundValue.isBlank()) {
|
||||
return expr;
|
||||
}
|
||||
String cleanValue = boundValue;
|
||||
if (cleanValue.startsWith("\"") && cleanValue.endsWith("\"")) {
|
||||
cleanValue = cleanValue.substring(1, cleanValue.length() - 1);
|
||||
}
|
||||
String suffix = cleanValue.contains(".") ? cleanValue.substring(cleanValue.lastIndexOf('.') + 1) : cleanValue;
|
||||
|
||||
Pattern eqPattern = Pattern.compile(
|
||||
"(?i)" + Pattern.quote(varName) + "\\s*==\\s*([\\w.\"']+)");
|
||||
Matcher matcher = eqPattern.matcher(expr);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String rhs = matcher.group(1).replace("\"", "").replace("'", "");
|
||||
boolean matches = cleanValue.equals(rhs)
|
||||
|| cleanValue.endsWith("." + rhs)
|
||||
|| suffix.equalsIgnoreCase(rhs)
|
||||
|| cleanValue.equalsIgnoreCase(rhs);
|
||||
matcher.appendReplacement(sb, matches ? "true" : "false");
|
||||
}
|
||||
matcher.appendTail(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true}/{@code false} when both sides are compile-time string literals; otherwise {@code null}.
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
protected final ExpressionAccessClassifier expressionAccessClassifier;
|
||||
protected final AccessorResolver accessorResolver;
|
||||
protected final FieldInitializerFinder fieldInitializerFinder;
|
||||
protected final PathBindingEvaluator pathBindingEvaluator;
|
||||
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
||||
|
||||
private ASTNode parseExpressionString(String expr) {
|
||||
@@ -91,6 +92,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
this.accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
||||
this.constantExtractor.setAccessorResolver(this.accessorResolver);
|
||||
this.fieldInitializerFinder = new FieldInitializerFinder(context);
|
||||
this.pathBindingEvaluator = new PathBindingEvaluator(context, variableTracer, constantResolver, typeResolver);
|
||||
}
|
||||
|
||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
@@ -111,6 +113,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
|
||||
for (List<String> path : uniquePaths) {
|
||||
if (!pathBindingEvaluator.isPathCompatible(path, callGraph, pathFinder)) {
|
||||
continue;
|
||||
}
|
||||
foundAny = true;
|
||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
||||
if (resolvedTp != null) {
|
||||
@@ -1281,25 +1286,17 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (currentTd != null) {
|
||||
String receiverType = context.getSuperclassFqn(currentTd);
|
||||
if (receiverType != null) {
|
||||
Optional<AccessorSummary> indexedSuperGetter =
|
||||
context.getAccessorIndex().lookup(receiverType, getterName);
|
||||
if (indexedSuperGetter.isPresent() && indexedSuperGetter.get().isGetter()) {
|
||||
CompilationUnit contextCu = currentTd.getRoot() instanceof CompilationUnit cu ? cu : null;
|
||||
if (indexedSuperGetter.get().kind()
|
||||
== click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
|
||||
List<String> constants = constantExtractor.resolveMethodReturnConstant(
|
||||
receiverType, getterName, 0, new HashSet<>(), contextCu);
|
||||
if (!constants.isEmpty()) {
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
||||
indexedSuperGetter.get(), contextCu, new HashSet<>());
|
||||
if (!fieldConstants.isEmpty()) {
|
||||
return fieldConstants;
|
||||
}
|
||||
CompilationUnit contextCu = currentTd.getRoot() instanceof CompilationUnit cu ? cu : null;
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
receiverType,
|
||||
getterName,
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration(receiverType), null, contextCu, false, null),
|
||||
ResolutionBudget.defaults());
|
||||
if (!resolved.isEmpty()) {
|
||||
return resolved;
|
||||
}
|
||||
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), null);
|
||||
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), contextCu);
|
||||
}
|
||||
}
|
||||
} else if (exprNode instanceof MethodInvocation mi) {
|
||||
@@ -1315,7 +1312,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
receiverType = variableTracer.getVariableDeclaredType(scopeMethod, receiverName);
|
||||
} else if (receiver instanceof ClassInstanceCreation cic) {
|
||||
directCic = cic;
|
||||
receiverType = cic.getType().toString();
|
||||
receiverType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||
} else if (receiver instanceof MethodInvocation receiverMi) {
|
||||
GetterChainReceiver chainedReceiver = resolveGetterChainToReceiver(receiverMi, scopeMethod);
|
||||
if (chainedReceiver != null) {
|
||||
|
||||
@@ -419,7 +419,8 @@ public class ConstantExtractor {
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
} else if (widenToImplementations) {
|
||||
}
|
||||
if (constants.isEmpty() && widenToImplementations) {
|
||||
List<String> impls = context.getImplementations(className);
|
||||
if (impls != null) {
|
||||
for (String implName : impls) {
|
||||
|
||||
@@ -170,13 +170,15 @@ public class ConstructorAnalyzer {
|
||||
}
|
||||
|
||||
if (results.isEmpty()) {
|
||||
// Context-Aware Fallback (Down): Check subclasses/implementations
|
||||
List<String> impls = context.getImplementations(fqn);
|
||||
if (impls != null) {
|
||||
for (String implFqn : impls) {
|
||||
TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
|
||||
if (implTd != null) {
|
||||
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited, budget));
|
||||
// Context-Aware Fallback (Down): widen only for interface/abstract types
|
||||
if (shouldWidenToImplementations(td)) {
|
||||
List<String> impls = context.getImplementations(fqn);
|
||||
if (impls != null) {
|
||||
for (String implFqn : impls) {
|
||||
TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
|
||||
if (implTd != null) {
|
||||
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited, budget));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,10 +189,11 @@ public class ConstructorAnalyzer {
|
||||
Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
|
||||
if (allTypes != null) {
|
||||
int matchedCount = 0;
|
||||
ResolutionBudget localBudget = ResolutionBudget.defaults();
|
||||
for (TypeDeclaration typeDecl : allTypes) {
|
||||
if (typeDecl == null) continue;
|
||||
if (context.hasField(typeDecl, fieldName)) {
|
||||
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited, budget));
|
||||
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited, localBudget));
|
||||
matchedCount++;
|
||||
if (matchedCount >= budget.globalFieldScanLimit()) {
|
||||
break;
|
||||
@@ -204,6 +207,13 @@ public class ConstructorAnalyzer {
|
||||
return results;
|
||||
}
|
||||
|
||||
private static boolean shouldWidenToImplementations(TypeDeclaration td) {
|
||||
if (td == null) {
|
||||
return true;
|
||||
}
|
||||
return td.isInterface() || Modifier.isAbstract(td.getModifiers());
|
||||
}
|
||||
|
||||
public Map<String, String> buildFieldValuesFromCIC(
|
||||
TypeDeclaration td,
|
||||
ClassInstanceCreation cic,
|
||||
|
||||
@@ -31,6 +31,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
private final SpringComponentDetector componentDetector;
|
||||
private List<TriggerPoint> cachedTriggers;
|
||||
private List<EntryPoint> cachedEntryPoints;
|
||||
private List<CallChain> cachedCallChains;
|
||||
|
||||
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||
this.context = context;
|
||||
@@ -93,8 +94,12 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
|
||||
@Override
|
||||
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
if (cachedCallChains != null) {
|
||||
return cachedCallChains;
|
||||
}
|
||||
log.info("JdtIntelligenceProvider searching for call chains between {} entry points and {} triggers", entryPoints.size(), triggers.size());
|
||||
return callGraphEngine.findChains(entryPoints, triggers);
|
||||
cachedCallChains = callGraphEngine.findChains(entryPoints, triggers);
|
||||
return cachedCallChains;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Walks a call path forward, propagating known parameter / local bindings so switch-case
|
||||
* edge constraints can prune impossible branches (e.g. {@code dispatch} arms).
|
||||
*/
|
||||
public class PathBindingEvaluator {
|
||||
|
||||
private final CodebaseContext context;
|
||||
private final VariableTracer variableTracer;
|
||||
private final ConstantResolver constantResolver;
|
||||
private final TypeResolver typeResolver;
|
||||
|
||||
public PathBindingEvaluator(
|
||||
CodebaseContext context,
|
||||
VariableTracer variableTracer,
|
||||
ConstantResolver constantResolver,
|
||||
TypeResolver typeResolver) {
|
||||
this.context = context;
|
||||
this.variableTracer = variableTracer;
|
||||
this.constantResolver = constantResolver;
|
||||
this.typeResolver = typeResolver;
|
||||
}
|
||||
|
||||
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
|
||||
Map<String, String> traceBindings(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
||||
Map<String, String> bindings = new HashMap<>();
|
||||
if (path == null || path.size() < 2) {
|
||||
return bindings;
|
||||
}
|
||||
for (int i = 0; i < path.size() - 1; i++) {
|
||||
String caller = path.get(i);
|
||||
String target = path.get(i + 1);
|
||||
CallEdge edge = findEdge(caller, target, callGraph, pathFinder);
|
||||
if (edge == null) {
|
||||
continue;
|
||||
}
|
||||
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
public boolean isPathCompatible(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
||||
if (path == null || path.size() < 2) {
|
||||
return true;
|
||||
}
|
||||
Map<String, String> bindings = new HashMap<>();
|
||||
for (int i = 0; i < path.size() - 1; i++) {
|
||||
String caller = path.get(i);
|
||||
String target = path.get(i + 1);
|
||||
CallEdge edge = findEdge(caller, target, callGraph, pathFinder);
|
||||
if (edge == null) {
|
||||
continue;
|
||||
}
|
||||
if (edge.getConstraint() != null
|
||||
&& !edge.getConstraint().isBlank()
|
||||
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(edge.getConstraint(), bindings)) {
|
||||
return false;
|
||||
}
|
||||
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void enrichBindings(
|
||||
String caller,
|
||||
String target,
|
||||
CallEdge edge,
|
||||
List<String> path,
|
||||
int pathIndex,
|
||||
Map<String, List<CallEdge>> callGraph,
|
||||
CallGraphPathFinder pathFinder,
|
||||
Map<String, String> bindings) {
|
||||
Map<String, String> paramValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, pathIndex);
|
||||
mergeResolvedBindings(bindings, paramValues, caller);
|
||||
|
||||
List<String> targetParams = typeResolver.getParameterNames(target);
|
||||
if (targetParams == null || edge.getArguments() == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < edge.getArguments().size() && i < targetParams.size(); i++) {
|
||||
String arg = edge.getArguments().get(i);
|
||||
String paramName = targetParams.get(i);
|
||||
String resolved = resolveBindingValue(caller, arg, bindings);
|
||||
if (shouldStoreBinding(paramName, resolved)) {
|
||||
bindings.put(paramName, resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldStoreBinding(String paramName, String resolved) {
|
||||
if (resolved == null || resolved.isBlank() || resolved.equals(paramName)) {
|
||||
return false;
|
||||
}
|
||||
if (resolved.contains("(") && !isEnumLikeConstant(resolved)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void mergeResolvedBindings(Map<String, String> bindings, Map<String, String> paramValues, String caller) {
|
||||
if (paramValues == null || paramValues.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<String, String> entry : paramValues.entrySet()) {
|
||||
String resolved = resolveBindingValue(caller, entry.getValue(), bindings);
|
||||
if (shouldStoreBinding(entry.getKey(), resolved)) {
|
||||
bindings.put(entry.getKey(), resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveBindingValue(String callerFqn, String rawValue, Map<String, String> bindings) {
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
if (rawValue.startsWith("\"") || isEnumLikeConstant(rawValue)) {
|
||||
return normalizeLiteral(rawValue);
|
||||
}
|
||||
if (bindings.containsKey(rawValue)) {
|
||||
return bindings.get(rawValue);
|
||||
}
|
||||
String traced = variableTracer.traceLocalVariable(callerFqn, rawValue, bindings);
|
||||
if (traced != null && !traced.isBlank()) {
|
||||
if (traced.startsWith("\"") || isEnumLikeConstant(traced)) {
|
||||
return traced;
|
||||
}
|
||||
String fromCall = resolveMethodCallFromSource(callerFqn, rawValue, bindings);
|
||||
if (fromCall != null) {
|
||||
return fromCall;
|
||||
}
|
||||
if (!traced.equals(rawValue)) {
|
||||
return traced;
|
||||
}
|
||||
}
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
private String resolveMethodCallFromSource(String callerFqn, String varName, Map<String, String> bindings) {
|
||||
MethodDeclaration md = findMethodDeclaration(callerFqn);
|
||||
if (md == null || md.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
final Expression[] initializer = new Expression[1];
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||
initializer[0] = node.getInitializer();
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
if (initializer[0] == null) {
|
||||
return null;
|
||||
}
|
||||
return evaluateExpressionValue(initializer[0], callerFqn, bindings);
|
||||
}
|
||||
|
||||
private String evaluateExpressionValue(Expression expr, String callerFqn, Map<String, String> bindings) {
|
||||
if (expr instanceof StringLiteral sl) {
|
||||
return "\"" + sl.getLiteralValue() + "\"";
|
||||
}
|
||||
if (expr instanceof QualifiedName qn) {
|
||||
return qn.getFullyQualifiedName();
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
return bindings.getOrDefault(sn.getIdentifier(), sn.getIdentifier());
|
||||
}
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
return evaluateMethodInvocationReturn(mi, callerFqn, bindings);
|
||||
}
|
||||
if (expr instanceof SwitchExpression se) {
|
||||
return constantResolver.evaluateSwitchWithParams(se, bindings, context);
|
||||
}
|
||||
String resolved = constantResolver.resolve(expr, context);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String evaluateMethodInvocationReturn(MethodInvocation mi, String callerFqn, Map<String, String> bindings) {
|
||||
String ownerFqn = resolveMethodOwnerFqn(mi, callerFqn);
|
||||
if (ownerFqn == null) {
|
||||
return null;
|
||||
}
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
TypeDeclaration owner = context.getTypeDeclaration(ownerFqn);
|
||||
if (owner == null) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration md = context.findMethodDeclaration(owner, methodName, true);
|
||||
if (md == null || md.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, String> callBindings = new HashMap<>(bindings);
|
||||
for (int i = 0; i < md.parameters().size() && i < mi.arguments().size(); i++) {
|
||||
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
|
||||
String paramName = param.getName().getIdentifier();
|
||||
String argValue = evaluateExpressionValue((Expression) mi.arguments().get(i), callerFqn, callBindings);
|
||||
if (argValue != null) {
|
||||
callBindings.put(paramName, argValue);
|
||||
}
|
||||
}
|
||||
|
||||
final String[] result = new String[1];
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() instanceof SwitchExpression se) {
|
||||
result[0] = constantResolver.evaluateSwitchWithParams(se, callBindings, context);
|
||||
} else if (node.getExpression() != null) {
|
||||
result[0] = evaluateExpressionValue(node.getExpression(), ownerFqn + "." + methodName, callBindings);
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return result[0];
|
||||
}
|
||||
|
||||
private String resolveFieldTypeFqn(String callerFqn, String fieldName) {
|
||||
if (callerFqn == null || !callerFqn.contains(".")) {
|
||||
return null;
|
||||
}
|
||||
String className = callerFqn.substring(0, callerFqn.lastIndexOf('.'));
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
for (FieldDeclaration field : td.getFields()) {
|
||||
for (Object fragmentObj : field.fragments()) {
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObj;
|
||||
if (fragment.getName().getIdentifier().equals(fieldName)) {
|
||||
IVariableBinding binding = fragment.resolveBinding();
|
||||
if (binding != null && binding.getType() != null) {
|
||||
return binding.getType().getErasure().getQualifiedName();
|
||||
}
|
||||
String simpleType = field.getType().toString();
|
||||
TypeDeclaration resolved = context.getTypeDeclaration(simpleType);
|
||||
if (resolved != null) {
|
||||
return context.getFqn(resolved);
|
||||
}
|
||||
if (td.getRoot() instanceof CompilationUnit cu && cu.getPackage() != null) {
|
||||
String pkg = cu.getPackage().getName().getFullyQualifiedName();
|
||||
resolved = context.getTypeDeclaration(pkg + "." + simpleType);
|
||||
if (resolved != null) {
|
||||
return context.getFqn(resolved);
|
||||
}
|
||||
}
|
||||
return simpleType;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveMethodOwnerFqn(MethodInvocation mi, String callerFqn) {
|
||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
||||
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
|
||||
return methodBinding.getDeclaringClass().getErasure().getQualifiedName();
|
||||
}
|
||||
Expression receiver = mi.getExpression();
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
return resolveFieldTypeFqn(callerFqn, sn.getIdentifier());
|
||||
}
|
||||
if (receiver instanceof FieldAccess fa) {
|
||||
IVariableBinding fieldBinding = fa.resolveFieldBinding();
|
||||
if (fieldBinding != null && fieldBinding.getType() != null) {
|
||||
return fieldBinding.getType().getErasure().getQualifiedName();
|
||||
}
|
||||
return resolveFieldTypeFqn(callerFqn, fa.getName().getIdentifier());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MethodDeclaration findMethodDeclaration(String methodFqn) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||
return null;
|
||||
}
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
return context.findMethodDeclaration(td, methodName, true);
|
||||
}
|
||||
|
||||
private static String normalizeLiteral(String value) {
|
||||
if (value != null && value.startsWith("\"") && value.endsWith("\"")) {
|
||||
return value.substring(1, value.length() - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean isEnumLikeConstant(String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int dot = value.lastIndexOf('.');
|
||||
if (dot <= 0 || dot >= value.length() - 1) {
|
||||
return false;
|
||||
}
|
||||
return Character.isUpperCase(value.charAt(dot + 1));
|
||||
}
|
||||
|
||||
private static CallEdge findEdge(
|
||||
String caller, String target, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
||||
List<CallEdge> edges = callGraph.get(caller);
|
||||
if (edges == null) {
|
||||
return null;
|
||||
}
|
||||
for (CallEdge edge : edges) {
|
||||
if (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||
return edge;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,28 @@ public class TypeResolver {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public List<String> getParameterNames(String methodFqn) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||
return null;
|
||||
}
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md == null) {
|
||||
return null;
|
||||
}
|
||||
List<String> paramNames = new ArrayList<>();
|
||||
for (Object paramObj : md.parameters()) {
|
||||
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
||||
paramNames.add(param.getName().getIdentifier());
|
||||
}
|
||||
return paramNames;
|
||||
}
|
||||
|
||||
public String getParameterName(String methodFqn, int index) {
|
||||
if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
|
||||
@@ -113,8 +113,100 @@ public final class AstUtils {
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
String switchConstraint = buildSwitchCaseConstraint(findEnclosingSwitchCase(node));
|
||||
if (switchConstraint != null) {
|
||||
conditions.add(switchConstraint);
|
||||
}
|
||||
if (conditions.isEmpty()) return null;
|
||||
java.util.Collections.reverse(conditions);
|
||||
return String.join(" && ", conditions);
|
||||
}
|
||||
|
||||
static org.eclipse.jdt.core.dom.SwitchCase findEnclosingSwitchCase(org.eclipse.jdt.core.dom.ASTNode node) {
|
||||
org.eclipse.jdt.core.dom.ASTNode current = node;
|
||||
while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
|
||||
if (current instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
|
||||
return switchCase;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
current = node;
|
||||
while (current != null && !(current instanceof org.eclipse.jdt.core.dom.MethodDeclaration)) {
|
||||
if (current instanceof org.eclipse.jdt.core.dom.YieldStatement yieldStatement) {
|
||||
org.eclipse.jdt.core.dom.ASTNode parent = yieldStatement.getParent();
|
||||
if (parent instanceof org.eclipse.jdt.core.dom.SwitchStatement switchStatement) {
|
||||
org.eclipse.jdt.core.dom.SwitchCase switchCase =
|
||||
findSwitchCaseForStatement(switchStatement.statements(), yieldStatement);
|
||||
if (switchCase != null) {
|
||||
return switchCase;
|
||||
}
|
||||
} else if (parent instanceof org.eclipse.jdt.core.dom.SwitchExpression switchExpression) {
|
||||
org.eclipse.jdt.core.dom.SwitchCase switchCase =
|
||||
findSwitchCaseForStatement(switchExpression.statements(), yieldStatement);
|
||||
if (switchCase != null) {
|
||||
return switchCase;
|
||||
}
|
||||
}
|
||||
} else if (current instanceof org.eclipse.jdt.core.dom.ExpressionStatement expressionStatement) {
|
||||
org.eclipse.jdt.core.dom.ASTNode parent = expressionStatement.getParent();
|
||||
if (parent instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
|
||||
return switchCase;
|
||||
}
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static org.eclipse.jdt.core.dom.SwitchCase findSwitchCaseForStatement(
|
||||
java.util.List<?> statements, org.eclipse.jdt.core.dom.ASTNode bodyStatement) {
|
||||
int idx = statements.indexOf(bodyStatement);
|
||||
if (idx < 0) {
|
||||
return null;
|
||||
}
|
||||
for (int i = idx - 1; i >= 0; i--) {
|
||||
if (statements.get(i) instanceof org.eclipse.jdt.core.dom.SwitchCase switchCase) {
|
||||
return switchCase;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String buildSwitchCaseConstraint(org.eclipse.jdt.core.dom.SwitchCase switchCase) {
|
||||
if (switchCase == null || switchCase.isDefault()) {
|
||||
return null;
|
||||
}
|
||||
org.eclipse.jdt.core.dom.Expression switchSelector = null;
|
||||
org.eclipse.jdt.core.dom.ASTNode switchParent = switchCase.getParent();
|
||||
if (switchParent instanceof org.eclipse.jdt.core.dom.SwitchStatement ss) {
|
||||
switchSelector = ss.getExpression();
|
||||
} else if (switchParent instanceof org.eclipse.jdt.core.dom.SwitchExpression se) {
|
||||
switchSelector = se.getExpression();
|
||||
}
|
||||
if (switchSelector == null) {
|
||||
return null;
|
||||
}
|
||||
java.util.List<String> caseLabels = new java.util.ArrayList<>();
|
||||
for (Object exprObj : switchCase.expressions()) {
|
||||
if (exprObj instanceof org.eclipse.jdt.core.dom.Expression caseExpr) {
|
||||
caseLabels.add(caseExpr.toString());
|
||||
}
|
||||
}
|
||||
if (caseLabels.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String selector = switchSelector.toString();
|
||||
if (caseLabels.size() == 1) {
|
||||
return selector + " == " + caseLabels.get(0);
|
||||
}
|
||||
StringBuilder combined = new StringBuilder("(");
|
||||
for (int i = 0; i < caseLabels.size(); i++) {
|
||||
if (i > 0) {
|
||||
combined.append(" || ");
|
||||
}
|
||||
combined.append(selector).append(" == ").append(caseLabels.get(i));
|
||||
}
|
||||
combined.append(')');
|
||||
return combined.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +323,9 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||
int depth) {
|
||||
if (!context.isInlineAccessors()) {
|
||||
return List.of();
|
||||
}
|
||||
if (ResolutionBudget.defaults().isAccessorInlineExhausted(depth)) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -361,9 +364,17 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
mi.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||
}
|
||||
|
||||
if (accessor.isPresent()
|
||||
&& shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
|
||||
return List.of();
|
||||
if (accessor.isPresent()) {
|
||||
String accessorOwner = accessor.get().ownerFqn();
|
||||
if (accessorOwner.equals(declaringFqn)) {
|
||||
Optional<AccessorSummary> concreteAccessor =
|
||||
lookupConcreteAccessorOverride(declaringFqn, methodName, receiverDefs);
|
||||
if (concreteAccessor.isPresent()) {
|
||||
accessor = concreteAccessor;
|
||||
} else if (shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (accessor.isEmpty()) {
|
||||
@@ -412,6 +423,23 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
return results;
|
||||
}
|
||||
|
||||
private Optional<AccessorSummary> lookupConcreteAccessorOverride(
|
||||
String declaringFqn,
|
||||
String methodName,
|
||||
List<Expression> receiverDefs) {
|
||||
for (Expression receiverDef : receiverDefs) {
|
||||
String concreteFqn = concreteReceiverTypeFqn(receiverDef);
|
||||
if (concreteFqn == null || concreteFqn.equals(declaringFqn)) {
|
||||
continue;
|
||||
}
|
||||
Optional<AccessorSummary> concreteAccessor = context.getAccessorIndex().lookup(concreteFqn, methodName);
|
||||
if (concreteAccessor.isPresent() && concreteAccessor.get().isGetter()) {
|
||||
return concreteAccessor;
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private boolean shouldSkipAccessorDueToConcreteOverride(
|
||||
String declaringFqn,
|
||||
String methodName,
|
||||
|
||||
Reference in New Issue
Block a user