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 28bb539..c809889 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 @@ -120,6 +120,30 @@ import java.util.*; if (paramIndex < edge.getArguments().size()) { String arg = edge.getArguments().get(paramIndex); if (arg != null) { + String receiver = edge.getReceiver(); + String actualReceiver = null; + if ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super.")) { + if (i - 1 > 0) { + String prevCaller = path.get(i - 2); + List prevEdges = callGraph.get(prevCaller); + if (prevEdges != null) { + for (CallEdge prevEdge : prevEdges) { + if (prevEdge.getTargetMethod().equals(caller) || isHeuristicMatch(prevEdge.getTargetMethod(), caller)) { + actualReceiver = prevEdge.getReceiver(); + break; + } + } + } + } + if (actualReceiver == null) { + actualReceiver = "this"; + } + if ("this".equals(arg) || arg.startsWith("this.")) { + arg = arg.replaceFirst("^this", actualReceiver); + } else if ("super".equals(arg) || arg.startsWith("super.")) { + arg = arg.replaceFirst("^super", actualReceiver); + } + } // If the argument passed has a method call, extract it String[] extractedArg = extractMethodSuffix(arg, methodSuffix); arg = extractedArg[0]; @@ -166,6 +190,25 @@ import java.util.*; } } + boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null + && !currentParamName.contains(".") && !currentParamName.contains("(") && !currentParamName.contains("new "); + if (hasGetterOnReceiver) { + List getterEvents = evaluateGetterOnReceiver(resolvedValue, 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(); + } + } + String tracedVar = traceLocalVariable(entryMethod, currentParamName); if (tracedVar != null && !tracedVar.equals(currentParamName)) { String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix); @@ -2413,4 +2456,125 @@ import java.util.*; } return null; } + + protected List evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List path, Map> callGraph) { + 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); + } + + if (receiverType == null && path.size() > 1) { + String callerMethod = path.get(1); + receiverType = getVariableDeclaredType(callerMethod, receiverName); + } + + if (receiverType != null) { + String simpleReceiverType = receiverType; + if (receiverType.contains("<")) { + String rawType = receiverType.substring(0, receiverType.indexOf('<')); + if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || context.getTypeDeclaration(rawType) == null) { + int start = receiverType.indexOf('<'); + int end = receiverType.lastIndexOf('>'); + if (start >= 0 && end > start) { + simpleReceiverType = receiverType.substring(start + 1, end); + } + } else { + simpleReceiverType = rawType; + } + } + TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType); + if (td != null) { + Map fieldValues = new HashMap<>(); + String varName = receiverName; + org.eclipse.jdt.core.dom.Expression initExpr = findVariableInitializer(entryMethod, varName); + if (initExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation initMi) { + initExpr = unwrapMethodInvocation(initMi, 0); + } + if (!(initExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic)) { + return null; + } + MethodDeclaration getter = null; + if (cic.getAnonymousClassDeclaration() != null) { + for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) { + if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) { + getter = md; + break; + } + } + } + if (getter == null) { + getter = context.findMethodDeclaration(td, getterName, true); + } + if (getter != null && getter.getBody() != null) { + fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor)); + 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<>()); + if (result != null) { + String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null; + if (returnType != null && !result.contains(".")) { + int lastDotRet = returnType.lastIndexOf('.'); + String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType; + if (!simpleReturnType.equals("String") && !simpleReturnType.equals("Integer") && !simpleReturnType.equals("Long") && !simpleReturnType.equals("Boolean")) { + result = simpleReturnType + "." + result; + } + } + return Collections.singletonList(result); + } + } + } + } + } + return null; + } + + protected 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]; + } } \ No newline at end of file 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 b73f86c..29622f0 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 @@ -540,341 +540,4 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { }); return foundReceiver[0]; } - - @Override - protected TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List path, Map> 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) { - java.util.Map parameterValues = buildParameterValuesMap(caller, target, callGraph, path, i); - String tracedVar = traceLocalVariable(target, currentParamName, parameterValues); - 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 edges = callGraph.get(caller); - boolean found = false; - if (edges != null) { - for (CallEdge edge : edges) { - if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { - String expectedType = getParameterType(target, paramIndex); - if (expectedType != null && paramIndex < edge.getArguments().size()) { - String argValue = edge.getArguments().get(paramIndex); - String actualType = null; - if (argValue.contains(".") && !argValue.contains("(")) { - String prefix = argValue.substring(0, argValue.lastIndexOf('.')); - if (!prefix.equals("this") && !prefix.equals("super")) { - actualType = prefix; - } - } - if (actualType == null && !argValue.contains(".")) { - actualType = getVariableDeclaredType(caller, argValue); - } - if (actualType != null && !isTypeCompatible(actualType, expectedType)) { - log.debug("Call rejected due to type mismatch: {} calling {} with type {} instead of expected {}", caller, edge.getTargetMethod(), actualType, expectedType); - continue; - } - } - if (paramIndex < edge.getArguments().size()) { - String arg = edge.getArguments().get(paramIndex); - if (arg != null) { - 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 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 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 (resolvedValue != null && resolvedValue.contains(".") && !resolvedValue.contains("(") && !resolvedValue.contains(" ")) { - 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()) - .polymorphicEvents(tp.getPolymorphicEvents() != null ? tp.getPolymorphicEvents() : new ArrayList<>()) - .build(); - return super.resolveTriggerPointParameters(modifiedTp, path, callGraph); - } - - 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 setterEvents = new ArrayList<>(); - List 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) { - List 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()) - .polymorphicEvents(tp.getPolymorphicEvents() != null ? tp.getPolymorphicEvents() : new ArrayList<>()) - .build(); - return super.resolveTriggerPointParameters(modifiedTp, path, callGraph); - } - - return super.resolveTriggerPointParameters(tp, path, callGraph); - } - - private List evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List path, Map> callGraph) { - 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); - } - - if (receiverType == null && path.size() > 1) { - String callerMethod = path.get(1); - receiverType = getVariableDeclaredType(callerMethod, receiverName); - } - - if (receiverType != null) { - String simpleReceiverType = receiverType; - if (receiverType.contains("<")) { - String rawType = receiverType.substring(0, receiverType.indexOf('<')); - if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || context.getTypeDeclaration(rawType) == null) { - int start = receiverType.indexOf('<'); - int end = receiverType.lastIndexOf('>'); - if (start >= 0 && end > start) { - simpleReceiverType = receiverType.substring(start + 1, end); - } - } else { - simpleReceiverType = rawType; - } - } - TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType); - if (td != null) { - Map fieldValues = new HashMap<>(); - String varName = receiverName; - org.eclipse.jdt.core.dom.Expression initExpr = findVariableInitializer(entryMethod, varName); - if (initExpr instanceof org.eclipse.jdt.core.dom.MethodInvocation initMi) { - initExpr = unwrapMethodInvocation(initMi, 0); - } - if (!(initExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic)) { - return null; - } - MethodDeclaration getter = null; - if (cic.getAnonymousClassDeclaration() != null) { - for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) { - if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) { - getter = md; - break; - } - } - } - if (getter == null) { - getter = context.findMethodDeclaration(td, getterName, true); - } - if (getter != null && getter.getBody() != null) { - fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor)); - 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<>()); - if (result != null) { - String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null; - if (returnType != null && !result.contains(".")) { - int lastDotRet = returnType.lastIndexOf('.'); - String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType; - if (!simpleReturnType.equals("String") && !simpleReturnType.equals("Integer") && !simpleReturnType.equals("Long") && !simpleReturnType.equals("Boolean")) { - 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]; - } - } \ No newline at end of file