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:
2026-07-11 20:49:16 +02:00
parent 8fa6a44e75
commit 8a97bceca4
24 changed files with 1208 additions and 1218 deletions

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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())

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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}.
*/

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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('.'));

View File

@@ -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();
}
}

View File

@@ -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,