This commit is contained in:
2026-06-24 18:03:19 +02:00
parent e2f8be9c6b
commit ae75379f77
2 changed files with 85 additions and 118 deletions

View File

@@ -12,7 +12,7 @@ import org.eclipse.jdt.core.dom.*;
import java.util.*;
@Slf4j
public abstract class AbstractCallGraphEngine implements CallGraphEngine {
public abstract class AbstractCallGraphEngine implements CallGraphEngine {
protected final CodebaseContext context;
protected final ConstantResolver constantResolver;
@@ -21,6 +21,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
this.constantResolver = new ConstantResolver();
}
@FunctionalInterface
protected interface RhsResolver {
String resolve(Expression rhs, Map<String, String> paramValues, TypeDeclaration td);
}
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
Map<String, List<CallEdge>> callGraph = buildCallGraph();
List<CallChain> chains = new ArrayList<>();
@@ -986,11 +991,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
* field assignments found in the matching constructor body.
*
* <p>Handles both {@code this.field = param} and bare {@code field = param} assignment forms.
*
* @param rhsResolver optional strategy to resolve non-SimpleName RHS expressions (e.g., method calls)
*/
protected java.util.Map<String, String> buildFieldValuesFromCIC(
org.eclipse.jdt.core.dom.TypeDeclaration td,
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
CodebaseContext context) {
CodebaseContext context,
RhsResolver rhsResolver) {
// Use the CIC's actual type as the starting point, not the declared type
String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
@@ -1001,16 +1009,27 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
java.util.Map<String, String> fieldValues = new java.util.HashMap<>();
java.util.Set<String> visitedCtors = new java.util.HashSet<>();
buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors);
buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors, rhsResolver);
return fieldValues;
}
/**
* Backward-compatible overload without RhsResolver.
*/
protected java.util.Map<String, String> buildFieldValuesFromCIC(
org.eclipse.jdt.core.dom.TypeDeclaration td,
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
CodebaseContext context) {
return buildFieldValuesFromCIC(td, cic, context, null);
}
private void buildFieldValuesFromCICRecursive(
org.eclipse.jdt.core.dom.TypeDeclaration td,
org.eclipse.jdt.core.dom.ClassInstanceCreation cic,
CodebaseContext context,
java.util.Map<String, String> fieldValues,
java.util.Set<String> visitedCtors) {
java.util.Set<String> visitedCtors,
RhsResolver rhsResolver) {
String tdKey = context.getFqn(td);
if (!visitedCtors.add(tdKey)) return;
@@ -1053,8 +1072,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
fieldName = sn.getIdentifier();
}
if (fieldName != null && rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
String paramVal = paramValues.get(snRhs.getIdentifier());
if (fieldName != null) {
String paramVal = null;
if (rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
paramVal = paramValues.get(snRhs.getIdentifier());
} else if (rhsResolver != null) {
paramVal = rhsResolver.resolve(rhs, paramValues, td);
}
if (paramVal != null) {
fieldValues.put(fieldName, paramVal);
fieldValues.put("this." + fieldName, paramVal);
@@ -1072,7 +1097,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (superFqn != null) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
processSuperConstructorArgs(superTd, sci.arguments(), paramValues, context, fieldValues, visitedCtors);
processSuperConstructorArgs(superTd, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
}
}
return super.visit(sci);
@@ -1088,7 +1113,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
java.util.Map<String, String> subClassParamValues,
CodebaseContext context,
java.util.Map<String, String> fieldValues,
java.util.Set<String> visitedCtors) {
java.util.Set<String> visitedCtors,
RhsResolver rhsResolver) {
if (superTd == null) return;
@@ -1141,8 +1167,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
fieldName = sn.getIdentifier();
}
if (fieldName != null && rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
String paramVal = superParamValues.get(snRhs.getIdentifier());
if (fieldName != null) {
String paramVal = null;
if (rhs instanceof org.eclipse.jdt.core.dom.SimpleName snRhs) {
paramVal = superParamValues.get(snRhs.getIdentifier());
} else if (rhsResolver != null) {
paramVal = rhsResolver.resolve(rhs, superParamValues, superTd);
}
if (paramVal != null) {
fieldValues.put(fieldName, paramVal);
fieldValues.put("this." + fieldName, paramVal);
@@ -1160,7 +1192,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (superFqn != null) {
TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
if (nextSuperTd != null) {
processSuperConstructorArgs(nextSuperTd, sci.arguments(), superParamValues, context, fieldValues, visitedCtors);
processSuperConstructorArgs(nextSuperTd, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
}
}
return super.visit(sci);
@@ -1170,6 +1202,40 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
}
}
/**
* Evaluates a method call expression found in a constructor body (e.g., {@code this.field = computeValue(param)}).
* Handles both explicit {@code this.} and implicit {@code this} (null receiver) calls.
*
* @param rhs the expression to evaluate (expected to be a MethodInvocation)
* @param paramValues map of constructor parameter names to their resolved values
* @param td the type declaration containing the method
* @return the resolved value, or {@code null} if evaluation is not possible
*/
protected String evaluateMethodCallInConstructor(
org.eclipse.jdt.core.dom.Expression rhs,
java.util.Map<String, String> paramValues,
org.eclipse.jdt.core.dom.TypeDeclaration td) {
if (!(rhs instanceof org.eclipse.jdt.core.dom.MethodInvocation mi)) {
return null;
}
String methodName = mi.getName().getIdentifier();
org.eclipse.jdt.core.dom.Expression receiver = mi.getExpression();
// Handle both explicit 'this.' and implicit 'this' (null receiver)
if (receiver != null && !(receiver instanceof org.eclipse.jdt.core.dom.ThisExpression)) {
return null;
}
org.eclipse.jdt.core.dom.MethodDeclaration method = context.findMethodDeclaration(td, methodName, true);
if (method == null || method.getBody() == null) {
return null;
}
java.util.Map<String, String> localVars = new java.util.HashMap<>(paramValues);
return constantResolver.evaluateMethodBodyWithLocals(method, localVars, context, new java.util.HashSet<>());
}
/**
* Searches for a {@code return new SomeType(...)} statement in the given method, recursing
* into interface implementations when the method body is absent.

View File

@@ -434,7 +434,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
if (varTypeTd == null) return null;
// Evaluate the getter body with field values substituted from the CIC's constructor args
java.util.Map<String, String> fieldValues = buildFieldValuesFromCIC(varTypeTd, cic, context);
java.util.Map<String, String> fieldValues = buildFieldValuesFromCIC(varTypeTd, cic, context, this::evaluateMethodCallInConstructor);
if (fieldValues.isEmpty()) return null;
// Track setter calls on the variable between its initialization and the getter call
@@ -810,8 +810,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
org.eclipse.jdt.core.dom.Expression initExpr = findVariableInitializer(entryMethod, varName);
System.out.println("DEBUG evaluateGetterOnReceiver: initExpr=" + initExpr + " class=" + (initExpr != null ? initExpr.getClass().getName() : "null"));
if (initExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context));
extractFieldValuesFromConstructorBody(td, cic, fieldValues);
fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor));
System.out.println("DEBUG evaluateGetterOnReceiver: fieldValues=" + fieldValues);
int lastDot = entryMethod.lastIndexOf('.');
if (lastDot > 0) {
@@ -872,102 +871,4 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
});
return initExpr[0];
}
private void extractFieldValuesFromConstructorBody(TypeDeclaration td, ClassInstanceCreation cic, Map<String, String> fieldValues) {
System.out.println("DEBUG extractFieldValuesFromConstructorBody: called for " + cic.getType());
String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName);
if (actualTd == null) {
actualTd = td;
}
final TypeDeclaration finalTd = actualTd;
for (MethodDeclaration ctor : finalTd.getMethods()) {
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
if (ctor.parameters().size() != cic.arguments().size()) continue;
Map<String, String> paramValues = new HashMap<>();
for (int i = 0; i < ctor.parameters().size(); i++) {
String pName = ((SingleVariableDeclaration) ctor.parameters().get(i)).getName().getIdentifier();
Expression argExpr = (Expression) cic.arguments().get(i);
List<String> consts = new ArrayList<>();
extractConstantsFromArgument(argExpr, consts);
if (!consts.isEmpty()) {
paramValues.put(pName, consts.get(0));
} else {
String resolved = constantResolver.resolve(argExpr, context);
if (resolved != null) paramValues.put(pName, resolved);
}
}
if (paramValues.isEmpty()) continue;
ctor.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(Assignment node) {
Expression lhs = node.getLeftHandSide();
Expression rhs = node.getRightHandSide();
String fieldName = null;
if (lhs instanceof FieldAccess fa && fa.getExpression() instanceof ThisExpression) {
fieldName = fa.getName().getIdentifier();
} else if (lhs instanceof SimpleName sn && !paramValues.containsKey(sn.getIdentifier())) {
fieldName = sn.getIdentifier();
}
if (fieldName != null) {
String rhsValue = null;
if (rhs instanceof SimpleName snRhs) {
rhsValue = paramValues.get(snRhs.getIdentifier());
System.out.println("DEBUG extractFieldValuesFromConstructorBody: found param assignment " + fieldName + "=" + rhsValue);
} else if (rhs instanceof MethodInvocation mi) {
rhsValue = evaluateMethodCallInConstructor(mi, paramValues, finalTd);
System.out.println("DEBUG extractFieldValuesFromConstructorBody: found method call assignment " + fieldName + "=" + rhsValue);
}
if (rhsValue != null) {
fieldValues.put(fieldName, rhsValue);
fieldValues.put("this." + fieldName, rhsValue);
}
}
return super.visit(node);
}
});
break; // first matching constructor is enough
}
}
private String evaluateMethodCallInConstructor(MethodInvocation mi, Map<String, String> paramValues, TypeDeclaration td) {
System.err.println("DEBUG evaluateMethodCallInConstructor: called for " + mi.getName().getIdentifier() + " in " + context.getFqn(td));
try {
String methodName = mi.getName().getIdentifier();
Expression receiver = mi.getExpression();
System.err.println("DEBUG evaluateMethodCallInConstructor: receiver=" + (receiver != null ? receiver.getClass().getName() : "null"));
// Handle both explicit 'this.' and implicit 'this' (null receiver)
if (receiver != null && !(receiver instanceof ThisExpression)) {
System.err.println("DEBUG evaluateMethodCallInConstructor: receiver not ThisExpression, returning null");
return null;
}
System.err.println("DEBUG evaluateMethodCallInConstructor: before findMethodDeclaration");
MethodDeclaration method = context.findMethodDeclaration(td, methodName, true);
System.err.println("DEBUG evaluateMethodCallInConstructor: method found=" + (method != null));
if (method != null) {
System.err.println("DEBUG evaluateMethodCallInConstructor: method in " + context.getFqn(findEnclosingType(method)) + " body=" + (method.getBody() != null));
}
if (method == null || method.getBody() == null) {
System.err.println("DEBUG evaluateMethodCallInConstructor: method null or no body, returning null");
return null;
}
Map<String, String> localVars = new HashMap<>(paramValues);
System.err.println("DEBUG evaluateMethodCallInConstructor: localVars=" + localVars);
String result = constantResolver.evaluateMethodBodyWithLocals(method, localVars, context, new HashSet<>());
System.err.println("DEBUG evaluateMethodCallInConstructor: result=" + result);
return result;
} catch (Exception e) {
System.err.println("DEBUG evaluateMethodCallInConstructor: EXCEPTION: " + e.getMessage());
e.printStackTrace();
return null;
}
}
}