diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index c8557bb..6bf758e 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -12,14 +12,19 @@ import org.eclipse.jdt.core.dom.*; import java.util.*; @Slf4j -public abstract class AbstractCallGraphEngine implements CallGraphEngine { - protected final CodebaseContext context; - protected final ConstantResolver constantResolver; + public abstract class AbstractCallGraphEngine implements CallGraphEngine { + protected final CodebaseContext context; + protected final ConstantResolver constantResolver; - public AbstractCallGraphEngine(CodebaseContext context) { - this.context = context; - this.constantResolver = new ConstantResolver(); - } + public AbstractCallGraphEngine(CodebaseContext context) { + this.context = context; + this.constantResolver = new ConstantResolver(); + } + + @FunctionalInterface + protected interface RhsResolver { + String resolve(Expression rhs, Map paramValues, TypeDeclaration td); + } public List findChains(List entryPoints, List triggers) { Map> callGraph = buildCallGraph(); @@ -986,11 +991,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { * field assignments found in the matching constructor body. * *

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 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 fieldValues = new java.util.HashMap<>(); java.util.Set 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 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 fieldValues, - java.util.Set visitedCtors) { + java.util.Set 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 subClassParamValues, CodebaseContext context, java.util.Map fieldValues, - java.util.Set visitedCtors) { + java.util.Set 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 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 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. diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index f859952..488e7fa 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -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 fieldValues = buildFieldValuesFromCIC(varTypeTd, cic, context); + java.util.Map 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 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 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 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 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 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; - } - } } \ No newline at end of file