te
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CallEdge {
|
||||
private final String targetMethod;
|
||||
private final List<String> arguments;
|
||||
private String targetMethod;
|
||||
private List<String> arguments;
|
||||
private String receiver;
|
||||
|
||||
public CallEdge(String targetMethod, List<String> arguments) {
|
||||
this(targetMethod, arguments, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1315,7 +1315,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List<String> constants) {
|
||||
protected void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List<String> constants) {
|
||||
if (argObj instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation innerCic) {
|
||||
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
|
||||
if ("String".equals(typeName)) {
|
||||
|
||||
@@ -35,7 +35,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
for (String calledMethod : calledMethods) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
|
||||
}
|
||||
for (Object argObj : node.arguments()) {
|
||||
if (argObj instanceof ExpressionMethodReference emr) {
|
||||
@@ -51,7 +52,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs, receiver));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -70,11 +72,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
} else {
|
||||
implicitArgs.addAll(args);
|
||||
}
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs, receiver));
|
||||
|
||||
List<String> impls = context.getImplementations(fallbackTypeFqn);
|
||||
for (String impl : impls) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args));
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(impl + "." + emr.getName().getIdentifier(), args, receiver));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +109,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
calledMethod = superFqn + "." + methodName;
|
||||
}
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
String receiver = "super";
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args, receiver));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,6 +169,13 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
|
||||
String val = null;
|
||||
|
||||
// Preserve getter calls on 'this' or 'super' for later resolution in trigger parameter tracing
|
||||
// where we can substitute the actual receiver from the call edge
|
||||
boolean isThisOrSuperGetter = expr instanceof MethodInvocation mi
|
||||
&& (mi.getExpression() instanceof ThisExpression || mi.getExpression() instanceof SuperMethodInvocation)
|
||||
&& mi.arguments().isEmpty();
|
||||
|
||||
if (!isThisOrSuperGetter) {
|
||||
// Narrow resolution for "localVar.getX()" patterns where localVar is initialized
|
||||
// from a ClassInstanceCreation (directly or via a factory method). This avoids
|
||||
// the broad ENUM_SET fallback when the getter transforms one enum to another.
|
||||
@@ -178,6 +189,9 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
if (val == null) {
|
||||
val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc)
|
||||
}
|
||||
} else {
|
||||
val = expr.toString(); // Preserve "this.getTransitionType()" for later substitution
|
||||
}
|
||||
args.add(val);
|
||||
}
|
||||
System.out.println("DEBUG resolveArguments: " + args);
|
||||
@@ -195,6 +209,12 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||
|
||||
String definingClass = findDefiningClass(className, methodName);
|
||||
if (definingClass != null && !definingClass.equals(className)) {
|
||||
allResolved.set(0, definingClass + "." + methodName);
|
||||
className = definingClass;
|
||||
}
|
||||
|
||||
List<String> impls = context.getImplementations(className);
|
||||
for (String impl : impls) {
|
||||
allResolved.add(impl + "." + methodName);
|
||||
@@ -204,6 +224,31 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return allResolved;
|
||||
}
|
||||
|
||||
private String findDefiningClass(String className, String methodName) {
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) return null;
|
||||
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
|
||||
if (md != null) {
|
||||
return className;
|
||||
}
|
||||
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
while (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
if (superTd != null) {
|
||||
MethodDeclaration superMd = context.findMethodDeclaration(superTd, methodName, false);
|
||||
if (superMd != null) {
|
||||
return superFqn;
|
||||
}
|
||||
superFqn = context.getSuperclassFqn(superTd);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String resolveCalledMethod(MethodInvocation node) {
|
||||
Expression receiver = node.getExpression();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
@@ -487,4 +532,442 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a string representation of the receiver expression from a MethodInvocation.
|
||||
* Handles ThisExpression, SimpleName, and other expression types.
|
||||
*/
|
||||
protected String getReceiverString(Expression receiver) {
|
||||
if (receiver == null) {
|
||||
return null;
|
||||
}
|
||||
if (receiver instanceof ThisExpression) {
|
||||
return "this";
|
||||
}
|
||||
if (receiver instanceof SuperMethodInvocation) {
|
||||
return "super";
|
||||
}
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
return sn.getIdentifier();
|
||||
}
|
||||
return receiver.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the receiver expression used in the caller method when calling the target method.
|
||||
* Walks the caller's AST to find a MethodInvocation with the target method name
|
||||
* and returns the receiver string.
|
||||
*/
|
||||
protected String findCallerReceiver(String callerFqn, String targetMethodFqn) {
|
||||
int lastDot = callerFqn.lastIndexOf('.');
|
||||
if (lastDot < 0) return null;
|
||||
String callerClass = callerFqn.substring(0, lastDot);
|
||||
String callerMethod = callerFqn.substring(lastDot + 1);
|
||||
|
||||
int targetLastDot = targetMethodFqn.lastIndexOf('.');
|
||||
if (targetLastDot < 0) return null;
|
||||
String targetMethodName = targetMethodFqn.substring(targetLastDot + 1);
|
||||
|
||||
TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
|
||||
if (callerTd == null) return null;
|
||||
|
||||
MethodDeclaration callerMd = context.findMethodDeclaration(callerTd, callerMethod, false);
|
||||
if (callerMd == null || callerMd.getBody() == null) return null;
|
||||
|
||||
final String[] foundReceiver = {null};
|
||||
callerMd.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if (foundReceiver[0] != null) return false;
|
||||
String called = resolveCalledMethod(node);
|
||||
if (called != null && (called.equals(targetMethodFqn) || called.endsWith("." + targetMethodName))) {
|
||||
foundReceiver[0] = getReceiverString(node.getExpression());
|
||||
return false;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
return foundReceiver[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
if (path.size() < 2) return tp;
|
||||
|
||||
String event = tp.getEvent();
|
||||
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
|
||||
|
||||
String currentParamName = event;
|
||||
String resolvedValue = event;
|
||||
String methodSuffix = "";
|
||||
boolean didThisSuperSubstitution = false;
|
||||
|
||||
String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix);
|
||||
currentParamName = extractedEntry[0];
|
||||
methodSuffix = extractedEntry[1];
|
||||
|
||||
for (int i = path.size() - 1; i > 0; i--) {
|
||||
String target = path.get(i);
|
||||
String caller = path.get(i - 1);
|
||||
int paramIndex = getParameterIndex(target, currentParamName);
|
||||
if (paramIndex < 0) {
|
||||
String tracedVar = traceLocalVariable(target, currentParamName);
|
||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||
String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix);
|
||||
tracedVar = extractedTraced[0];
|
||||
methodSuffix = extractedTraced[1];
|
||||
currentParamName = tracedVar;
|
||||
resolvedValue = tracedVar + methodSuffix;
|
||||
paramIndex = getParameterIndex(target, currentParamName);
|
||||
}
|
||||
}
|
||||
if (paramIndex < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
List<CallEdge> edges = callGraph.get(caller);
|
||||
boolean found = false;
|
||||
if (edges != null) {
|
||||
for (CallEdge edge : edges) {
|
||||
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||
if (paramIndex < edge.getArguments().size()) {
|
||||
String arg = edge.getArguments().get(paramIndex);
|
||||
if (arg != null) {
|
||||
System.out.println("DEBUG TRACE: caller=" + caller + " target=" + target + " paramIndex=" + paramIndex + " arg=" + arg + " receiver=" + edge.getReceiver());
|
||||
String receiver = edge.getReceiver();
|
||||
if (receiver != null && !receiver.isEmpty() && ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super."))) {
|
||||
String actualReceiver = receiver;
|
||||
boolean needPrevReceiver = "this".equals(arg) || arg.startsWith("this.");
|
||||
if (needPrevReceiver && !"this".equals(receiver) && !"super".equals(receiver)) {
|
||||
if (i - 1 > 0) {
|
||||
String prevCaller = path.get(i - 2);
|
||||
List<CallEdge> prevEdges = callGraph.get(prevCaller);
|
||||
if (prevEdges != null) {
|
||||
for (CallEdge prevEdge : prevEdges) {
|
||||
if (prevEdge.getTargetMethod().equals(caller) || isHeuristicMatch(prevEdge.getTargetMethod(), caller)) {
|
||||
actualReceiver = prevEdge.getReceiver();
|
||||
System.out.println("DEBUG TRACE: found prev edge, actualReceiver=" + actualReceiver);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("this".equals(receiver) || "super".equals(receiver)) {
|
||||
if (i - 1 > 0) {
|
||||
String prevCaller = path.get(i - 2);
|
||||
List<CallEdge> prevEdges = callGraph.get(prevCaller);
|
||||
if (prevEdges != null) {
|
||||
for (CallEdge prevEdge : prevEdges) {
|
||||
if (prevEdge.getTargetMethod().equals(caller) || isHeuristicMatch(prevEdge.getTargetMethod(), caller)) {
|
||||
actualReceiver = prevEdge.getReceiver();
|
||||
System.out.println("DEBUG TRACE: found prev edge, actualReceiver=" + actualReceiver);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (actualReceiver != null && !actualReceiver.isEmpty()) {
|
||||
if ("this".equals(arg) || arg.startsWith("this.")) {
|
||||
arg = arg.replaceFirst("^this", actualReceiver);
|
||||
didThisSuperSubstitution = true;
|
||||
} else if ("super".equals(arg) || arg.startsWith("super.")) {
|
||||
arg = arg.replaceFirst("^super", actualReceiver);
|
||||
didThisSuperSubstitution = true;
|
||||
}
|
||||
System.out.println("DEBUG TRACE: after substitution arg=" + arg);
|
||||
}
|
||||
}
|
||||
String[] extractedArg = extractMethodSuffix(arg, methodSuffix);
|
||||
arg = extractedArg[0];
|
||||
methodSuffix = extractedArg[1];
|
||||
currentParamName = arg;
|
||||
resolvedValue = arg + methodSuffix;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) break;
|
||||
}
|
||||
|
||||
String entryMethod = path.get(0);
|
||||
int entryParamIndex = getParameterIndex(entryMethod, currentParamName);
|
||||
if (entryParamIndex < 0) {
|
||||
if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) {
|
||||
String localMethodName = methodSuffix.substring(1).replace("()", "");
|
||||
org.eclipse.jdt.core.dom.Expression localSetterExpr = traceLocalSetter(entryMethod, currentParamName, localMethodName);
|
||||
if (localSetterExpr != null) {
|
||||
List<String> setterEvents = new ArrayList<>();
|
||||
List<org.eclipse.jdt.core.dom.Expression> tracedSetters = traceVariableAll(localSetterExpr);
|
||||
for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) {
|
||||
extractConstantsFromExpression(tracedSetter, setterEvents);
|
||||
}
|
||||
if (!setterEvents.isEmpty()) {
|
||||
return TriggerPoint.builder()
|
||||
.event(tp.getEvent())
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.sourceModule(tp.getSourceModule())
|
||||
.stateMachineId(tp.getStateMachineId())
|
||||
.sourceState(tp.getSourceState())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.polymorphicEvents(setterEvents)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null
|
||||
&& !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new ");
|
||||
if (!hasGetterOnReceiver) {
|
||||
String tracedVar = traceLocalVariable(entryMethod, currentParamName);
|
||||
if (tracedVar != null && !tracedVar.equals(currentParamName)) {
|
||||
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
|
||||
tracedVar = extractedFinalTraced[0];
|
||||
methodSuffix = extractedFinalTraced[1];
|
||||
currentParamName = tracedVar;
|
||||
resolvedValue = tracedVar + methodSuffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String finalResolvedValue = resolvedValue;
|
||||
boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null
|
||||
&& !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new ");
|
||||
if (hasGetterOnReceiver && didThisSuperSubstitution) {
|
||||
List<String> getterEvents = evaluateGetterOnReceiver(finalResolvedValue, methodSuffix, entryMethod, path, callGraph);
|
||||
if (getterEvents != null && !getterEvents.isEmpty()) {
|
||||
return TriggerPoint.builder()
|
||||
.event(tp.getEvent())
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.sourceModule(tp.getSourceModule())
|
||||
.stateMachineId(tp.getStateMachineId())
|
||||
.sourceState(tp.getSourceState())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.polymorphicEvents(getterEvents)
|
||||
.build();
|
||||
}
|
||||
// If we couldn't evaluate the getter, fall back to super with modified trigger point
|
||||
TriggerPoint modifiedTp = TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.sourceModule(tp.getSourceModule())
|
||||
.stateMachineId(tp.getStateMachineId())
|
||||
.sourceState(tp.getSourceState())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.build();
|
||||
return super.resolveTriggerPointParameters(modifiedTp, path, callGraph);
|
||||
}
|
||||
|
||||
return super.resolveTriggerPointParameters(tp, path, callGraph);
|
||||
}
|
||||
|
||||
private List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
System.out.println("DEBUG evaluateGetterOnReceiver: resolvedValue=" + resolvedValue + " methodSuffix=" + methodSuffix + " entryMethod=" + entryMethod);
|
||||
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17);
|
||||
exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
|
||||
exprParser.setSource(resolvedValue.toCharArray());
|
||||
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null);
|
||||
|
||||
if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) {
|
||||
String getterName = mi.getName().getIdentifier();
|
||||
org.eclipse.jdt.core.dom.Expression receiver = mi.getExpression();
|
||||
String receiverName = null;
|
||||
String receiverType = null;
|
||||
|
||||
if (receiver instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
|
||||
receiverName = sn.getIdentifier();
|
||||
receiverType = getVariableDeclaredType(entryMethod, receiverName);
|
||||
System.out.println("DEBUG evaluateGetterOnReceiver: receiverName=" + receiverName + " receiverType=" + receiverType);
|
||||
}
|
||||
|
||||
if (receiverType == null && path.size() > 1) {
|
||||
String callerMethod = path.get(1);
|
||||
receiverType = getVariableDeclaredType(callerMethod, receiverName);
|
||||
System.out.println("DEBUG evaluateGetterOnReceiver: fallback receiverType=" + receiverType);
|
||||
}
|
||||
|
||||
if (receiverType != null) {
|
||||
String simpleReceiverType = receiverType;
|
||||
if (receiverType.contains("<")) {
|
||||
simpleReceiverType = receiverType.substring(0, receiverType.indexOf('<'));
|
||||
}
|
||||
System.out.println("DEBUG evaluateGetterOnReceiver: simpleReceiverType=" + simpleReceiverType);
|
||||
TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType);
|
||||
if (td != null) {
|
||||
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
|
||||
if (getter != null && getter.getBody() != null) {
|
||||
Map<String, String> fieldValues = new HashMap<>();
|
||||
String varName = receiverName;
|
||||
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);
|
||||
System.out.println("DEBUG evaluateGetterOnReceiver: fieldValues=" + fieldValues);
|
||||
int lastDot = entryMethod.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
String className = entryMethod.substring(0, lastDot);
|
||||
String methodName = entryMethod.substring(lastDot + 1);
|
||||
TypeDeclaration entryTd = context.getTypeDeclaration(className);
|
||||
if (entryTd != null) {
|
||||
MethodDeclaration entryMd = context.findMethodDeclaration(entryTd, methodName, false);
|
||||
if (entryMd != null && entryMd.getBody() != null) {
|
||||
// Skip trackSetterCallsBetween since getter call is not in entry method
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
|
||||
System.out.println("DEBUG evaluateGetterOnReceiver: result=" + result);
|
||||
if (result != null) {
|
||||
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
|
||||
if (returnType != null && !result.contains(".")) {
|
||||
int lastDot = returnType.lastIndexOf('.');
|
||||
String simpleReturnType = lastDot > 0 ? returnType.substring(lastDot + 1) : returnType;
|
||||
result = simpleReturnType + "." + result;
|
||||
}
|
||||
return Collections.singletonList(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private org.eclipse.jdt.core.dom.Expression findVariableInitializer(String methodFqn, String varName) {
|
||||
int lastDot = methodFqn.lastIndexOf('.');
|
||||
if (lastDot < 0) return null;
|
||||
String className = methodFqn.substring(0, lastDot);
|
||||
String methodName = methodFqn.substring(lastDot + 1);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td == null) return null;
|
||||
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
|
||||
if (md == null || md.getBody() == null) return null;
|
||||
|
||||
final org.eclipse.jdt.core.dom.Expression[] initExpr = {null};
|
||||
md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.VariableDeclarationStatement node) {
|
||||
for (Object fragObj : node.fragments()) {
|
||||
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
|
||||
if (frag.getName().getIdentifier().equals(varName) && frag.getInitializer() != null) {
|
||||
initExpr[0] = frag.getInitializer();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
|
||||
class HeuristicCallGraphEngineExtendedTest {
|
||||
/**
|
||||
* Diagnostic: Generics, inherited processing, multi-enum constructor args,
|
||||
* and internal constructor mapping via a switch method.
|
||||
* <p>
|
||||
* Pattern:
|
||||
* 1. Abstract base class AbstractEventClass<T> holds the payload and transitionType.
|
||||
* 2. Abstract base class also contains the method that actually fires the state machine.
|
||||
* 3. MyEventClass<T> constructor takes multiple enums (InputEnum1, InputEnum2).
|
||||
* 4. MyEventClass assigns: this.transitionType = computeTransitionType(InputEnum2).
|
||||
* 5. computeTransitionType uses a switch to map InputEnum2 to TransitionEnum.
|
||||
* <p>
|
||||
* Root cause targeted: The engine must not eagerly grab InputEnum1.IGNORE or
|
||||
* InputEnum2.SOURCE_B. It must evaluate the internal computeTransitionType()
|
||||
* method during instantiation to resolve the final TransitionEnum.STATE_Y.
|
||||
*/
|
||||
@Test
|
||||
void shouldResolveTransitionEnumThroughGenericsAndInternalConstructorSwitchComputation() throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
|
||||
public class SystemController {
|
||||
private StateMachine machine;
|
||||
|
||||
public void processIncomingPayload(String rawData) {
|
||||
// Creates concrete class with multiple enums and a generic payload.
|
||||
// Engine MUST NOT stop at IGNORE or SOURCE_B.
|
||||
MyEventClass<String> event = new MyEventClass<>(
|
||||
rawData,
|
||||
InputEnum1.IGNORE,
|
||||
InputEnum2.SOURCE_B
|
||||
);
|
||||
|
||||
// Calls the inherited method that actually processes the event
|
||||
event.fireEvent(machine);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractEventClass<T> {
|
||||
protected T payload;
|
||||
protected TransitionEnum transitionType;
|
||||
|
||||
public AbstractEventClass(T payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
// The method to actually process events
|
||||
public void fireEvent(StateMachine machine) {
|
||||
machine.fire(this.getTransitionType());
|
||||
}
|
||||
|
||||
public TransitionEnum getTransitionType() {
|
||||
return this.transitionType;
|
||||
}
|
||||
}
|
||||
|
||||
class MyEventClass<T> extends AbstractEventClass<T> {
|
||||
private InputEnum1 routingFlag;
|
||||
|
||||
public MyEventClass(T payload, InputEnum1 flag, InputEnum2 source) {
|
||||
super(payload);
|
||||
this.routingFlag = flag;
|
||||
|
||||
// The trap: Assignment via an internal method evaluation
|
||||
this.transitionType = computeTransitionType(source);
|
||||
}
|
||||
|
||||
private TransitionEnum computeTransitionType(InputEnum2 source) {
|
||||
return switch (source) {
|
||||
case SOURCE_A -> TransitionEnum.STATE_X;
|
||||
case SOURCE_B -> TransitionEnum.STATE_Y; // Expected resolution
|
||||
case SOURCE_C -> TransitionEnum.STATE_Z;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
public void fire(TransitionEnum event) {}
|
||||
}
|
||||
|
||||
enum InputEnum1 { IGNORE, PROCESS_ASYNC }
|
||||
enum InputEnum2 { SOURCE_A, SOURCE_B, SOURCE_C }
|
||||
enum TransitionEnum { STATE_X, STATE_Y, STATE_Z }
|
||||
""";
|
||||
|
||||
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_generics_computation");
|
||||
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.SystemController")
|
||||
.methodName("processIncomingPayload")
|
||||
.build();
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
|
||||
System.out.println("GENERICS & COMPUTATION polymorphicEvents: " +
|
||||
chains.get(0).getTriggerPoint().getPolymorphicEvents());
|
||||
|
||||
// The assertion to prove the bug is fixed.
|
||||
// If the engine returns InputEnum2.SOURCE_B or InputEnum1.IGNORE, it fails.
|
||||
// It MUST evaluate computeTransitionType() and return TransitionEnum.STATE_Y.
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder("TransitionEnum.STATE_Y");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user