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 c64442a..2d8f18d 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,19 +12,38 @@ 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(); - } + protected final TypeResolver typeResolver; + protected final CallGraphPathFinder pathFinder; + protected final VariableTracer variableTracer; + protected final ConstantExtractor constantExtractor; + protected final ConstructorAnalyzer constructorAnalyzer; - @FunctionalInterface - protected interface RhsResolver { - String resolve(Expression rhs, Map paramValues, TypeDeclaration td); - } + @FunctionalInterface + protected interface RhsResolver { + String resolve(Expression rhs, Map paramValues, TypeDeclaration td); + } + + public AbstractCallGraphEngine(CodebaseContext context) { + this.context = context; + this.constantResolver = new ConstantResolver(); + + this.typeResolver = new TypeResolver(context); + this.pathFinder = new CallGraphPathFinder(context); + this.variableTracer = new VariableTracer(context, constantResolver); + this.constantExtractor = new ConstantExtractor(context, constantResolver, this::resolveCalledMethod); + this.constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver); + + // Wire up cross dependencies + this.variableTracer.setConstantExtractor(this.constantExtractor); + this.constantExtractor.setVariableTracer(this.variableTracer); + this.constantExtractor.setConstructorAnalyzer(this.constructorAnalyzer); + this.constructorAnalyzer.setVariableTracer(this.variableTracer); + this.constructorAnalyzer.setConstantExtractor(this.constantExtractor); + } public List findChains(List entryPoints, List triggers) { Map> callGraph = buildCallGraph(); @@ -35,11 +54,11 @@ import java.util.*; boolean foundAny = false; for (TriggerPoint tp : triggers) { String targetMethod = tp.getClassName() + "." + tp.getMethodName(); - List path = findPath(startMethod, targetMethod, callGraph, new HashSet<>()); + List path = pathFinder.findPath(startMethod, targetMethod, callGraph, new HashSet<>()); if (path != null) { foundAny = true; TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph); - String contextMachineId = extractContextMachineId(path, callGraph); + String contextMachineId = pathFinder.extractContextMachineId(path, callGraph); chains.add(CallChain.builder() .entryPoint(ep) .triggerPoint(resolvedTp) @@ -65,42 +84,36 @@ import java.util.*; String resolvedValue = event; String methodSuffix = ""; - // Extract method calls like .getType() so we can trace the base parameter String[] extractedEntry = extractMethodSuffix(currentParamName, methodSuffix); currentParamName = extractedEntry[0]; methodSuffix = extractedEntry[1]; - // Walk backwards up the call chain for (int i = path.size() - 1; i > 0; i--) { String target = path.get(i); String caller = path.get(i - 1); - // Find parameter index in target method - int paramIndex = getParameterIndex(target, currentParamName); + int paramIndex = typeResolver.getParameterIndex(target, currentParamName); if (paramIndex < 0) { - // Not a parameter. Maybe it's a local variable initialized from a parameter or method call? - Map parameterValues = buildParameterValuesMap(caller, target, callGraph, path, i); - String tracedVar = traceLocalVariable(target, currentParamName, parameterValues); + Map parameterValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, i); + String tracedVar = variableTracer.traceLocalVariable(target, currentParamName, parameterValues); if (tracedVar != null && !tracedVar.equals(currentParamName)) { - // Extract method calls like .getType() from the traced variable String[] extractedTraced = extractMethodSuffix(tracedVar, methodSuffix); tracedVar = extractedTraced[0]; methodSuffix = extractedTraced[1]; currentParamName = tracedVar; resolvedValue = tracedVar + methodSuffix; - paramIndex = getParameterIndex(target, currentParamName); + paramIndex = typeResolver.getParameterIndex(target, currentParamName); } } if (paramIndex < 0) { - break; // Parameter name changed or not found, stop tracing + break; } - // Find the edge from caller to target to get the argument passed 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 (edge.getTargetMethod().equals(target) || pathFinder.isHeuristicMatch(edge.getTargetMethod(), target)) { + String expectedType = typeResolver.getParameterType(target, paramIndex); if (expectedType != null && paramIndex < edge.getArguments().size()) { String argValue = edge.getArguments().get(paramIndex); String actualType = null; @@ -111,9 +124,9 @@ import java.util.*; } } if (actualType == null && !argValue.contains(".")) { - actualType = getVariableDeclaredType(caller, argValue); + actualType = variableTracer.getVariableDeclaredType(caller, argValue); } - if (actualType != null && !isTypeCompatible(actualType, expectedType)) { + if (actualType != null && !typeResolver.isTypeCompatible(actualType, expectedType)) { continue; } } @@ -128,7 +141,7 @@ import java.util.*; List prevEdges = callGraph.get(prevCaller); if (prevEdges != null) { for (CallEdge prevEdge : prevEdges) { - if (prevEdge.getTargetMethod().equals(caller) || isHeuristicMatch(prevEdge.getTargetMethod(), caller)) { + if (prevEdge.getTargetMethod().equals(caller) || pathFinder.isHeuristicMatch(prevEdge.getTargetMethod(), caller)) { actualReceiver = prevEdge.getReceiver(); break; } @@ -144,7 +157,6 @@ import java.util.*; arg = arg.replaceFirst("^super", actualReceiver); } } - // If the argument passed has a method call, extract it String[] extractedArg = extractMethodSuffix(arg, methodSuffix); arg = extractedArg[0]; methodSuffix = extractedArg[1]; @@ -157,22 +169,20 @@ import java.util.*; } } } - if (!found) break; // Could not map argument + if (!found) break; } - // Final check on the entry method String entryMethod = path.get(0); - int entryParamIndex = getParameterIndex(entryMethod, currentParamName); + int entryParamIndex = typeResolver.getParameterIndex(entryMethod, currentParamName); if (entryParamIndex < 0) { - // Intercept local setter before falling back to initializer 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); + Expression localSetterExpr = variableTracer.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); + List tracedSetters = variableTracer.traceVariableAll(localSetterExpr); + for (Expression tracedSetter : tracedSetters) { + constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents); } if (!setterEvents.isEmpty()) { return TriggerPoint.builder() @@ -209,7 +219,7 @@ import java.util.*; } } - String tracedVar = traceLocalVariable(entryMethod, currentParamName); + String tracedVar = variableTracer.traceLocalVariable(entryMethod, currentParamName); if (tracedVar != null && !tracedVar.equals(currentParamName)) { String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix); tracedVar = extractedFinalTraced[0]; @@ -223,7 +233,7 @@ import java.util.*; if (resolvedValue.startsWith("ENUM_SET:")) { for (String eVal : resolvedValue.substring(9).split(",")) { - String parsed = parseEnumSetElement(eVal); + String parsed = constantExtractor.parseEnumSetElement(eVal); if (!polymorphicEvents.contains(parsed)) polymorphicEvents.add(parsed); } return TriggerPoint.builder() @@ -239,46 +249,40 @@ import java.util.*; .build(); } - // Parse resolvedValue using JDT to robustly handle complex expressions - 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); + ASTParser exprParser = ASTParser.newParser(AST.JLS17); + exprParser.setKind(ASTParser.K_EXPRESSION); exprParser.setSource(resolvedValue.toCharArray()); - org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null); + ASTNode exprNode = exprParser.createAST(null); String varName = null; String methodName = null; String declaredType = null; String sourceMethod = null; - if (exprNode instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { + if (exprNode instanceof MethodInvocation mi) { methodName = mi.getName().getIdentifier(); - System.out.println("DEBUG mi.getName(): " + methodName + ", mi.getExpression() type: " + (mi.getExpression() != null ? mi.getExpression().getClass().getName() : "null")); - if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + if (mi.getExpression() instanceof SimpleName sn) { varName = sn.getIdentifier(); - } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe && - pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce && - ce.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + } else if (mi.getExpression() instanceof ParenthesizedExpression pe && + pe.getExpression() instanceof CastExpression ce && + ce.getExpression() instanceof SimpleName sn) { varName = sn.getIdentifier(); declaredType = ce.getType().toString(); sourceMethod = "inline-cast"; - } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { - System.out.println("DEBUG found ClassInstanceCreation!"); + } else if (mi.getExpression() instanceof ClassInstanceCreation cic) { declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); sourceMethod = "inline-instantiation"; if (cic.getAnonymousClassDeclaration() != null) { - System.out.println("DEBUG resolving anonymous class!"); final List polyEventsRef = polymorphicEvents; for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) { - if (declObj instanceof org.eclipse.jdt.core.dom.MethodDeclaration mDecl && mDecl.getName().getIdentifier().equals(methodName) && mDecl.getBody() != null) { - System.out.println("DEBUG visiting anonymous class method: " + methodName); - mDecl.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { + if (declObj instanceof MethodDeclaration mDecl && mDecl.getName().getIdentifier().equals(methodName) && mDecl.getBody() != null) { + mDecl.getBody().accept(new ASTVisitor() { @Override - public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) { + public boolean visit(ReturnStatement rs) { if (rs.getExpression() != null) { - System.out.println("DEBUG found return expression: " + rs.getExpression().toString()); - List tracedReturns = traceVariableAll(rs.getExpression()); - for (org.eclipse.jdt.core.dom.Expression tracedRet : tracedReturns) { - extractConstantsFromExpression(tracedRet, polyEventsRef); + List tracedReturns = variableTracer.traceVariableAll(rs.getExpression()); + for (Expression tracedRet : tracedReturns) { + constantExtractor.extractConstantsFromExpression(tracedRet, polyEventsRef); } } return super.visit(rs); @@ -287,9 +291,9 @@ import java.util.*; } } } - } else if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) { + } else if (mi.getExpression() instanceof MethodInvocation innerMi) { methodName = innerMi.getName().getIdentifier(); - if (innerMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName snInner) { + if (innerMi.getExpression() instanceof SimpleName snInner) { varName = snInner.getIdentifier(); } else { String exprStr = innerMi.getExpression() != null ? innerMi.getExpression().toString() : ""; @@ -298,43 +302,40 @@ import java.util.*; } } } else { - // Fallback for complex chained expressions String exprStr = mi.getExpression() != null ? mi.getExpression().toString() : "this"; if (!exprStr.contains("(")) { varName = exprStr; } } - } else if (exprNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + } else if (exprNode instanceof ClassInstanceCreation cic) { declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); sourceMethod = "inline-instantiation"; - polymorphicEvents.add(declaredType); // Track the payload type as the event - } else if (exprNode instanceof org.eclipse.jdt.core.dom.ConditionalExpression cond) { - if (cond.getThenExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicThen) { + polymorphicEvents.add(declaredType); + } else if (exprNode instanceof ConditionalExpression cond) { + if (cond.getThenExpression() instanceof ClassInstanceCreation cicThen) { polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicThen.getType())); } - if (cond.getElseExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cicElse) { + if (cond.getElseExpression() instanceof ClassInstanceCreation cicElse) { polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cicElse.getType())); } sourceMethod = "inline-ternary"; declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0); - } else if (exprNode instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + } else if (exprNode instanceof SimpleName sn) { varName = sn.getIdentifier(); - // Check if it's a constant (Java UPPER_SNAKE_CASE convention) - // If so, add it directly to polymorphicEvents instead of tracing as a variable if (varName.equals(varName.toUpperCase()) && varName.contains("_")) { polymorphicEvents.add(varName); - methodName = null; // Skip variable tracing + methodName = null; } else { - methodName = "VariableReference"; // We just want to trigger deep trace + methodName = "VariableReference"; } - } else if (exprNode instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe && - pe.getExpression() instanceof org.eclipse.jdt.core.dom.CastExpression ce && - ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + } else if (exprNode instanceof ParenthesizedExpression pe && + pe.getExpression() instanceof CastExpression ce && + ce.getExpression() instanceof ClassInstanceCreation cic) { declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); sourceMethod = "inline-instantiation"; polymorphicEvents.add(declaredType); - } else if (exprNode instanceof org.eclipse.jdt.core.dom.CastExpression ce && - ce.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + } else if (exprNode instanceof CastExpression ce && + ce.getExpression() instanceof ClassInstanceCreation cic) { declaredType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); sourceMethod = "inline-instantiation"; polymorphicEvents.add(declaredType); @@ -342,12 +343,12 @@ import java.util.*; if (methodName != null) { if (varName != null && !varName.isEmpty() && Character.isLowerCase(varName.charAt(0)) && !methodName.equals("VariableReference")) { - org.eclipse.jdt.core.dom.Expression localSetterExpr = traceLocalSetter(entryMethod, varName, methodName); + Expression localSetterExpr = variableTracer.traceLocalSetter(entryMethod, varName, methodName); if (localSetterExpr != null) { - List tracedSetters = traceVariableAll(localSetterExpr); - for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) { - extractConstantsFromExpression(tracedSetter, polymorphicEvents); - if (tracedSetter instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic && cic.getAnonymousClassDeclaration() != null) { + List tracedSetters = variableTracer.traceVariableAll(localSetterExpr); + for (Expression tracedSetter : tracedSetters) { + constantExtractor.extractConstantsFromExpression(tracedSetter, polymorphicEvents); + if (tracedSetter instanceof ClassInstanceCreation cic && cic.getAnonymousClassDeclaration() != null) { resolvedValue = cic.toString() + "." + methodName + "()"; } } @@ -380,13 +381,13 @@ import java.util.*; final String mName = methodName; final String[] finalDeclaredType = {null}; final String[] finalSourceMethod = {null}; - currentMd.accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - public boolean visit(org.eclipse.jdt.core.dom.VariableDeclarationStatement node) { + currentMd.accept(new ASTVisitor() { + public boolean visit(VariableDeclarationStatement node) { for (Object fragObj : node.fragments()) { - if (fragObj instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(targetVar)) { + if (fragObj instanceof VariableDeclarationFragment vdf && vdf.getName().getIdentifier().equals(targetVar)) { if (vdf.getInitializer() != null) { - org.eclipse.jdt.core.dom.Expression valueNode = traceVariable(vdf.getInitializer()); - if (valueNode instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + Expression valueNode = variableTracer.traceVariable(vdf.getInitializer()); + if (valueNode instanceof ClassInstanceCreation cic) { if (cic.getAnonymousClassDeclaration() != null) { extractedFinalTraced[0] = cic.toString(); extractedFinalTraced[1] = mName != null && !mName.equals("VariableReference") ? "." + mName + "()" : ""; @@ -410,17 +411,15 @@ import java.util.*; } if (extractedFinalTraced[0] != null) { resolvedValue = extractedFinalTraced[0] + extractedFinalTraced[1]; - System.out.println("DEBUG localized tracing resolvedValue to: " + resolvedValue); - // Since we updated resolvedValue, we need to extract from it if it's an inline anonymous class if (extractedFinalTraced[0].contains("new ") && extractedFinalTraced[0].contains("{")) { - org.eclipse.jdt.core.dom.ASTParser p = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17); - p.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION); + ASTParser p = ASTParser.newParser(AST.JLS17); + p.setKind(ASTParser.K_EXPRESSION); p.setSource(resolvedValue.toCharArray()); - org.eclipse.jdt.core.dom.ASTNode newNode = p.createAST(null); - if (newNode instanceof org.eclipse.jdt.core.dom.Expression) { - List traced = traceVariableAll((org.eclipse.jdt.core.dom.Expression) newNode); - for (org.eclipse.jdt.core.dom.Expression ex : traced) { - extractConstantsFromExpression(ex, polymorphicEvents); + ASTNode newNode = p.createAST(null); + if (newNode instanceof Expression) { + List traced = variableTracer.traceVariableAll((Expression) newNode); + for (Expression ex : traced) { + constantExtractor.extractConstantsFromExpression(ex, polymorphicEvents); } } } @@ -431,13 +430,11 @@ import java.util.*; if (declaredType == null) { for (String methodFqn : path) { - declaredType = getVariableDeclaredType(methodFqn, varName); + declaredType = variableTracer.getVariableDeclaredType(methodFqn, varName); if (declaredType != null) { - System.out.println("DEBUG getVariableDeclaredType(" + methodFqn + ", " + varName + ") = " + declaredType); TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))); CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null; - List values = resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu); - System.out.println("DEBUG resolveMethodReturnConstant returned: " + values); + List values = constantExtractor.resolveMethodReturnConstant(declaredType, methodName, 0, new HashSet<>(), cu); if (values != null && !values.isEmpty()) { polymorphicEvents.addAll(values); } @@ -445,9 +442,8 @@ import java.util.*; break; } } - // If it wasn't a variable, it might be a static method call (e.g., EventBuilder.buildEvent()) if (declaredType == null && varName.matches("^[A-Z].*")) { - org.eclipse.jdt.core.dom.TypeDeclaration staticTd = context.getTypeDeclaration(varName); + TypeDeclaration staticTd = context.getTypeDeclaration(varName); if (staticTd != null) { declaredType = context.getFqn(staticTd); sourceMethod = "static-call"; @@ -460,15 +456,11 @@ import java.util.*; } if (declaredType == null && methodName != null && !methodName.equals("VariableReference")) { - System.out.println("DEBUG fallback triggered for methodName: " + methodName); for (String methodFqn : path) { - System.out.println("DEBUG fallback checking path method: " + methodFqn); TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))); if (currentTd != null && context.findMethodDeclaration(currentTd, methodName, true) != null) { - System.out.println("DEBUG fallback found method in " + context.getFqn(currentTd)); CompilationUnit cu = currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null; - List values = resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu); - System.out.println("DEBUG fallback resolveMethodReturnConstant returned: " + values); + List values = constantExtractor.resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu); if (values != null && !values.isEmpty()) { polymorphicEvents.addAll(values); sourceMethod = methodFqn; @@ -478,47 +470,46 @@ import java.util.*; } } - if (declaredType != null) { - List enumValues = context.getEnumValues(declaredType); - if (enumValues != null && !enumValues.isEmpty()) { - for (String ev : enumValues) { - polymorphicEvents.add(parseEnumSetElement(ev)); - } + if (declaredType != null) { + List enumValues = context.getEnumValues(declaredType); + if (enumValues != null && !enumValues.isEmpty()) { + for (String ev : enumValues) { + polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev)); + } + } else { + List typesToInspect = new ArrayList<>(); + if ("inline-instantiation".equals(sourceMethod)) { + typesToInspect.add(declaredType); } else { - List typesToInspect = new ArrayList<>(); - if ("inline-instantiation".equals(sourceMethod)) { - typesToInspect.add(declaredType); + typesToInspect.add(declaredType); + typesToInspect.addAll(context.getImplementations(declaredType)); + } + + for (String type : typesToInspect) { + Set visited = new HashSet<>(); + TypeDeclaration baseTd = context.getTypeDeclaration(type); + CompilationUnit cuToUse = null; + if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) { + cuToUse = (CompilationUnit) baseTd.getRoot(); } else { - typesToInspect.add(declaredType); - typesToInspect.addAll(context.getImplementations(declaredType)); - } - - for (String type : typesToInspect) { - Set visited = new HashSet<>(); - TypeDeclaration baseTd = context.getTypeDeclaration(type); - CompilationUnit cuToUse = null; - if (baseTd != null && baseTd.getRoot() instanceof CompilationUnit) { - cuToUse = (CompilationUnit) baseTd.getRoot(); - } else { - String firstPathMethod = path.get(0); - String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.')); - TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName); - if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) { - cuToUse = (CompilationUnit) entryTd.getRoot(); - } + String firstPathMethod = path.get(0); + String entryClassName = firstPathMethod.substring(0, firstPathMethod.lastIndexOf('.')); + TypeDeclaration entryTd = context.getTypeDeclaration(entryClassName); + if (entryTd != null && entryTd.getRoot() instanceof CompilationUnit) { + cuToUse = (CompilationUnit) entryTd.getRoot(); } - List constants = resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse); - for (String constant : constants) { - if (!polymorphicEvents.contains(constant)) { - polymorphicEvents.add(constant); - } + } + List constants = constantExtractor.resolveMethodReturnConstant(type, methodName, 0, visited, cuToUse); + for (String constant : constants) { + if (!polymorphicEvents.contains(constant)) { + polymorphicEvents.add(constant); } } } } } + } - boolean hasValidConstant = false; for (String ev : polymorphicEvents) { String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev; @@ -528,17 +519,13 @@ import java.util.*; } } - // As a fallback, attempt direct AST extraction (e.g. from MessageBuilder chains or inline constructor args) - if (!hasValidConstant && exprNode instanceof org.eclipse.jdt.core.dom.Expression) { - extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) exprNode, polymorphicEvents); + if (!hasValidConstant && exprNode instanceof Expression) { + constantExtractor.extractConstantsFromExpression((Expression) exprNode, polymorphicEvents); } - // The aggressive regex scraper has been completely removed to avoid false positives. - // We rely on precise AST traversal instead. - List newPolyEvents = new ArrayList<>(); for (String pe : polymorphicEvents) { - List resolved = resolveClassConstantReturns(pe, context, null); + List resolved = constantExtractor.resolveClassConstantReturns(pe, context, null); if (resolved != null && !resolved.isEmpty()) { newPolyEvents.addAll(resolved); } else { @@ -547,12 +534,11 @@ import java.util.*; } polymorphicEvents = newPolyEvents; - // Clean up any remaining invalid AST fallbacks (like 'type', 'event', or unresolved class names) polymorphicEvents.removeIf(e -> { - if (e.contains(".")) return false; // If it's a fully qualified enum/constant, keep it + if (e.contains(".")) return false; String val = e; boolean isKnownEnumVal = false; - for (java.util.List vals : context.getEnumValuesMap().values()) { + for (List vals : context.getEnumValuesMap().values()) { for (String v : vals) { if (v.endsWith("." + val)) { isKnownEnumVal = true; @@ -566,11 +552,11 @@ import java.util.*; }); String targetMethod = path.get(path.size() - 1); - int eventParamIndex = getParameterIndex(targetMethod, event); + int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event); if (eventParamIndex < 0) { eventParamIndex = 0; } - String expectedType = getParameterType(targetMethod, eventParamIndex); + String expectedType = typeResolver.getParameterType(targetMethod, eventParamIndex); if (expectedType != null) { final String expType = expectedType; boolean isExpectedEnum = context.getEnumValues(expType) != null; @@ -579,7 +565,7 @@ import java.util.*; String actualType = e.substring(0, e.lastIndexOf('.')); boolean isActualEnum = context.getEnumValues(actualType) != null; if (isExpectedEnum && isActualEnum) { - return !isTypeCompatible(actualType, expType); + return !typeResolver.isTypeCompatible(actualType, expType); } } return false; @@ -602,1370 +588,6 @@ import java.util.*; return tp; } - protected int getParameterIndex(String methodFqn, String paramName) { - if (methodFqn == null || !methodFqn.contains(".")) return -1; - String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); - String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); - TypeDeclaration td = context.getTypeDeclaration(className); - if (td != null) { - MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md != null) { - for (int i = 0; i < md.parameters().size(); i++) { - SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i); - if (svd.getName().getIdentifier().equals(paramName)) { - return i; - } - } - } - } - return -1; - } - - protected String getParameterType(String methodFqn, int index) { - if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null; - String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); - String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); - TypeDeclaration td = context.getTypeDeclaration(className); - if (td != null) { - MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md != null && index < md.parameters().size()) { - SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(index); - return svd.getType().toString(); - } - } - return null; - } - - protected boolean isTypeCompatible(String actualType, String expectedType) { - if (actualType == null || expectedType == null) return true; - if (expectedType.equals("Object") || expectedType.equals("java.lang.Object")) return true; - if (actualType.equals("Object") || actualType.equals("java.lang.Object")) return true; - if (expectedType.length() == 1 && Character.isUpperCase(expectedType.charAt(0))) return true; - if (actualType.length() == 1 && Character.isUpperCase(actualType.charAt(0))) return true; - - // Save original expectedType to check generic type arguments - String originalExpectedType = expectedType; - - // Unwrap collections - if (actualType.contains("<")) { - String rawType = actualType.substring(0, actualType.indexOf('<')); - if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) { - int start = actualType.indexOf('<'); - int end = actualType.lastIndexOf('>'); - if (start >= 0 && end > start) { - actualType = actualType.substring(start + 1, end); - } - } - } - if (expectedType.contains("<")) { - String rawType = expectedType.substring(0, expectedType.indexOf('<')); - if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) { - int start = expectedType.indexOf('<'); - int end = expectedType.lastIndexOf('>'); - if (start >= 0 && end > start) { - expectedType = expectedType.substring(start + 1, end); - } - } - } - - // Strip any remaining generic arguments from raw types - if (actualType.contains("<")) { - actualType = actualType.substring(0, actualType.indexOf('<')); - } - if (expectedType.contains("<")) { - expectedType = expectedType.substring(0, expectedType.indexOf('<')); - } - - if (actualType.equals(expectedType)) return true; - if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true; - - AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType); - if (td != null) { - if (td instanceof TypeDeclaration typeDecl) { - String superFqn = context.getSuperclassFqn(typeDecl); - while (superFqn != null) { - if (superFqn.contains("<")) { - superFqn = superFqn.substring(0, superFqn.indexOf('<')); - } - if (superFqn.equals(expectedType) || superFqn.endsWith("." + expectedType) || expectedType.endsWith("." + superFqn)) return true; - TypeDeclaration superTd = context.getTypeDeclaration(superFqn); - superFqn = superTd != null ? context.getSuperclassFqn(superTd) : null; - } - } - List interfaces = java.util.Collections.emptyList(); - if (td instanceof TypeDeclaration typeDecl) { - interfaces = typeDecl.superInterfaceTypes(); - } else if (td instanceof EnumDeclaration enumDecl) { - interfaces = enumDecl.superInterfaceTypes(); - } else if (td instanceof RecordDeclaration recordDecl) { - interfaces = recordDecl.superInterfaceTypes(); - } - for (Object interfaceType : interfaces) { - String itStr = interfaceType.toString(); - if (itStr.contains("<")) { - itStr = itStr.substring(0, itStr.indexOf('<')); - } - if (itStr.equals(expectedType) || itStr.endsWith("." + expectedType) || expectedType.endsWith("." + itStr)) return true; - } - } - - // Fallback: Check if the actualType matches any of the generic type arguments of expectedType - if (originalExpectedType.contains("<")) { - int start = originalExpectedType.indexOf('<'); - int end = originalExpectedType.lastIndexOf('>'); - if (start >= 0 && end > start) { - String sub = originalExpectedType.substring(start + 1, end); - for (String part : sub.split(",")) { - String typeArg = part.trim(); - if (isTypeCompatible(actualType, typeArg)) { - return true; - } - } - } - } - return false; - } - - - public String getVariableDeclaredType(String methodFqn, String cleanFieldName) { - if (methodFqn == null) return null; - int lastDot = methodFqn.lastIndexOf('.'); - if (lastDot > 0) { - String fqn = methodFqn.substring(0, lastDot); - String methodName = methodFqn.substring(lastDot + 1); - TypeDeclaration td = context.getTypeDeclaration(fqn); - if (td != null) { - MethodDeclaration md = context.findMethodDeclaration(td, methodName, false); - if (md != null) { - for (Object pObj : md.parameters()) { - SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj; - if (svd.getName().getIdentifier().equals(cleanFieldName)) { - return svd.getType().toString(); - } - } - final String[] foundType = new String[1]; - if (md.getBody() != null) { - md.getBody().accept(new ASTVisitor() { - @Override - public boolean visit(VariableDeclarationStatement node) { - for (Object fragObj : node.fragments()) { - VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; - if (frag.getName().getIdentifier().equals(cleanFieldName)) { - foundType[0] = node.getType().toString(); - } - } - return super.visit(node); - } - - @Override - public boolean visit(org.eclipse.jdt.core.dom.LambdaExpression node) { - for (Object paramObj : node.parameters()) { - org.eclipse.jdt.core.dom.VariableDeclaration param = (org.eclipse.jdt.core.dom.VariableDeclaration) paramObj; - if (param.getName().getIdentifier().equals(cleanFieldName)) { - if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) { - foundType[0] = svd.getType().toString(); - return false; - } - - org.eclipse.jdt.core.dom.ASTNode parent = node.getParent(); - if (parent instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { - org.eclipse.jdt.core.dom.Expression caller = mi.getExpression(); - - // Phase 6: Stream API chains - boolean hasMap = false; - while (caller instanceof org.eclipse.jdt.core.dom.MethodInvocation chainMi) { - String mName = chainMi.getName().getIdentifier(); - if (mName.equals("map") || mName.equals("flatMap")) { - hasMap = true; - // Extract return type of the map lambda - if (chainMi.arguments().size() == 1 && chainMi.arguments().get(0) instanceof org.eclipse.jdt.core.dom.LambdaExpression lambda) { - if (lambda.getBody() instanceof org.eclipse.jdt.core.dom.MethodInvocation mapMi) { - // Just a heuristic for now: we use the method's return type if we could resolve it, but since we can't easily, - // we'll try to find the type of the caller and look up the method. - org.eclipse.jdt.core.dom.Expression mapCaller = mapMi.getExpression(); - if (mapCaller instanceof org.eclipse.jdt.core.dom.SimpleName mapSn) { - String mapCallerType = getVariableDeclaredType(methodFqn, mapSn.getIdentifier()); - if (mapCallerType != null) { - TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))); - CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null; - TypeDeclaration td = context.getTypeDeclaration(mapCallerType, cu); - if (td != null) { - MethodDeclaration mapMd = context.findMethodDeclaration(td, mapMi.getName().getIdentifier(), true); - if (mapMd != null && mapMd.getReturnType2() != null) { - foundType[0] = mapMd.getReturnType2().toString(); - return false; - } - } - } - } - } - } - } - if (mName.equals("stream") || mName.equals("filter") || mName.equals("map") || - mName.equals("flatMap") || mName.equals("peek") || mName.equals("distinct") || - mName.equals("sorted") || mName.equals("limit") || mName.equals("skip")) { - caller = chainMi.getExpression(); - } else { - break; - } - } - - if (!hasMap) { - if (caller instanceof org.eclipse.jdt.core.dom.SimpleName callerSn) { - String callerName = callerSn.getIdentifier(); - System.out.println("DEBUG callerName: " + callerName + " cleanFieldName: " + cleanFieldName); - if (!callerName.equals(cleanFieldName)) { - String callerType = getVariableDeclaredType(methodFqn, callerName); - System.out.println("DEBUG callerType for " + callerName + " = " + callerType); - if (callerType != null && callerType.contains("<")) { - int start = callerType.indexOf('<'); - int end = callerType.lastIndexOf('>'); - if (start >= 0 && end > start) { - foundType[0] = callerType.substring(start + 1, end); - System.out.println("DEBUG foundType[0]: " + foundType[0]); - return false; - } - } - } - } else if (caller instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { - String callerName = fa.getName().getIdentifier(); - if (!callerName.equals(cleanFieldName)) { - String callerType = getVariableDeclaredType(methodFqn, callerName); - if (callerType != null && callerType.contains("<")) { - int start = callerType.indexOf('<'); - int end = callerType.lastIndexOf('>'); - if (start >= 0 && end > start) { - foundType[0] = callerType.substring(start + 1, end); - return false; - } - } - } - } - } - } - } - } - return super.visit(node); - } - }); - } - if (foundType[0] != null) return foundType[0]; - } - - // Fallback to fields - for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) { - for (Object fragObj : fd.fragments()) { - org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj; - if (frag.getName().getIdentifier().equals(cleanFieldName)) { - return fd.getType().toString(); - } - } - } - } - } - return null; - } - - protected List resolveMethodReturnConstant(String className, String methodName, int depth, Set visited, CompilationUnit contextCu) { - if (depth > 20) return Collections.emptyList(); - String fqn = className + "." + methodName; - if (!visited.add(fqn)) return Collections.emptyList(); - - List constants = new ArrayList<>(); - TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null; - if (tempTd == null) { - tempTd = context.getTypeDeclaration(className); - } - final TypeDeclaration td = tempTd; - if (td == null) { - } - if (td != null) { - MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md != null && md.getBody() != null) { - md.getBody().accept(new ASTVisitor() { - @Override - public boolean visit(ReturnStatement node) { - Expression retExpr = node.getExpression(); - if (retExpr != null) { - boolean handled = false; - if (retExpr instanceof MethodInvocation mi) { - // Follow delegation first - String called = resolveCalledMethod(mi); - if (called != null && called.contains(".")) { - if (visited.contains(called)) { - handled = true; - } else { - String cName = called.substring(0, called.lastIndexOf('.')); - String mName = called.substring(called.lastIndexOf('.') + 1); - - TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); - if (targetTd == null) targetTd = context.getTypeDeclaration(cName); - - if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { - List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); - constants.addAll(delegationResult); - handled = true; - } - } - } - } else if (retExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi) { - String superFqn = context.getSuperclassFqn(td); - if (superFqn != null) { - String called = superFqn + "." + smi.getName().getIdentifier(); - if (visited.contains(called)) { - handled = true; - } else { - String cName = superFqn; - String mName = smi.getName().getIdentifier(); - TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); - if (targetTd == null) targetTd = context.getTypeDeclaration(cName); - - if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { - List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); - constants.addAll(delegationResult); - handled = true; - } - } - } - } - if (!handled) { - List tracedRetExprs = traceVariableAll(retExpr); - for (Expression tracedRetExpr : tracedRetExprs) { - extractConstantsFromExpression(tracedRetExpr, constants); - String val = constantResolver.resolve(tracedRetExpr, context); - if (val != null) { - if (val.startsWith("ENUM_SET:")) { - for (String eVal : val.substring(9).split(",")) { - String parsed = parseEnumSetElement(eVal); - if (!constants.contains(parsed)) constants.add(parsed); - } - } else { - if (!constants.contains(val)) constants.add(val); - } - } else if (tracedRetExpr instanceof org.eclipse.jdt.core.dom.SimpleName sn) { - List consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited); - if (!consts.isEmpty()) { - constants.addAll(consts); - } - } else if (tracedRetExpr instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { - List consts = traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited); - if (!consts.isEmpty()) { - constants.addAll(consts); - } - } - } - } - } - return super.visit(node); - } - }); - } else { - List impls = context.getImplementations(className); - if (impls != null) { - for (String implName : impls) { - List delegationResult = resolveMethodReturnConstant(implName, methodName, depth + 1, visited, contextCu); - constants.addAll(delegationResult); - } - } - } - } - visited.remove(fqn); - return constants; - } - - protected List resolveClassConstantReturns(String className, click.kamil.springstatemachineexporter.ast.common.CodebaseContext context, CompilationUnit contextCu) { - if (className != null && className.contains(".")) { - String prefix = className.substring(0, className.lastIndexOf('.')); - String suffix = className.substring(className.lastIndexOf('.') + 1); - TypeDeclaration prefixTd = contextCu != null ? context.getTypeDeclaration(prefix, contextCu) : context.getTypeDeclaration(prefix); - if (prefixTd == null) prefixTd = context.getTypeDeclaration(prefix); - boolean isKnownType = prefixTd != null || context.getEnumValues(prefix) != null; - if (isKnownType) { - String simplePrefix = prefix.contains(".") ? prefix.substring(prefix.lastIndexOf('.') + 1) : prefix; - return java.util.Collections.singletonList(simplePrefix + "." + suffix); - } - } - - TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className); - if (td == null) td = context.getTypeDeclaration(className); - if (td == null) return null; - - final List resolvedConstants = new ArrayList<>(); - for (MethodDeclaration md : td.getMethods()) { - if (md.getBody() != null) { - md.getBody().accept(new ASTVisitor() { - public boolean visit(ReturnStatement node) { - List tracedReturns = traceVariableAll(node.getExpression()); - for (org.eclipse.jdt.core.dom.Expression tracedRet : tracedReturns) { - extractConstantsFromExpression(tracedRet, resolvedConstants); - } - return super.visit(node); - } - }); - } - if (!resolvedConstants.isEmpty()) { - return resolvedConstants; - } - } - return null; - } - - protected void extractConstantsFromExpression(org.eclipse.jdt.core.dom.Expression expr, List constants) { - if (expr instanceof QualifiedName qn) { - constants.add(qn.toString()); - } else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) { - constants.add(sl.getLiteralValue()); - } else if (expr instanceof org.eclipse.jdt.core.dom.ConditionalExpression ce) { - extractConstantsFromExpression(ce.getThenExpression(), constants); - extractConstantsFromExpression(ce.getElseExpression(), constants); - } else if (expr instanceof org.eclipse.jdt.core.dom.ParenthesizedExpression pe) { - extractConstantsFromExpression(pe.getExpression(), constants); - } else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) { - extractConstantsFromExpression(ce.getExpression(), constants); - } else if (expr instanceof org.eclipse.jdt.core.dom.Assignment assignment) { - extractConstantsFromExpression(assignment.getRightHandSide(), constants); - } else if (expr instanceof org.eclipse.jdt.core.dom.SwitchExpression se) { - se.accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.YieldStatement ys) { - extractConstantsFromExpression(ys.getExpression(), constants); - return super.visit(ys); - } - @Override - public boolean visit(org.eclipse.jdt.core.dom.ExpressionStatement es) { - extractConstantsFromExpression(es.getExpression(), constants); - return super.visit(es); - } - @Override - public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) { - extractConstantsFromExpression(rs.getExpression(), constants); - return super.visit(rs); - } - }); - } else if (expr instanceof org.eclipse.jdt.core.dom.ArrayAccess aa) { - extractConstantsFromExpression(aa.getArray(), constants); - } else if (expr instanceof org.eclipse.jdt.core.dom.ArrayCreation ac && ac.getInitializer() != null) { - for (Object expObj : ac.getInitializer().expressions()) { - extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) expObj, constants); - } - } else if (expr instanceof org.eclipse.jdt.core.dom.ArrayInitializer ai) { - for (Object expObj : ai.expressions()) { - extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) expObj, constants); - } - - } else if (expr instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { - String methodName = mi.getName().getIdentifier(); - - // Phase 7: List.get(0) indexing - if (methodName.equals("get") && mi.arguments().size() == 1 && mi.getExpression() != null) { - extractConstantsFromExpression(mi.getExpression(), constants); - return; - } - // Phase 7: List.of("A", "B") or Arrays.asList("A", "B") - if ((methodName.equals("of") || methodName.equals("asList")) && mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName) { - for (Object argObj : mi.arguments()) { - extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) argObj, constants); - } - return; - } - - - - // 1. Local setter tracking - if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn) { - String varName = sn.getIdentifier(); - String propName = methodName.startsWith("get") ? methodName.substring(3) : methodName; - - org.eclipse.jdt.core.dom.Block block = findEnclosingBlock(mi); - if (block != null) { - for (Object stmtObj : block.statements()) { - if (stmtObj == mi.getParent() || stmtObj == mi) break; - if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) { - if (es.getExpression() instanceof org.eclipse.jdt.core.dom.MethodInvocation setterMi) { - if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) { - if (setterMi.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName setterSn && setterSn.getIdentifier().equals(varName)) { - if (!setterMi.arguments().isEmpty()) { - extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) setterMi.arguments().get(0), constants); - return; - } - } - } - } else if (es.getExpression() instanceof org.eclipse.jdt.core.dom.Assignment assignment) { - if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { - if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName faSn && faSn.getIdentifier().equals(varName)) { - if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) { - extractConstantsFromExpression(assignment.getRightHandSide(), constants); - return; - } - } - } else if (assignment.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.QualifiedName qqn) { - if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) { - if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) { - extractConstantsFromExpression(assignment.getRightHandSide(), constants); - return; - } - } - } - } - } - } - } - } - - // 2. Inline Instantiation Getter (e.g. new Event(CONSTANT).getType()) - if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { - if (cic.getAnonymousClassDeclaration() != null) { - for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) { - if (declObj instanceof org.eclipse.jdt.core.dom.MethodDeclaration mdecl) { - if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) { - mdecl.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) { - if (rs.getExpression() != null) { - extractConstantsFromExpression(rs.getExpression(), constants); - } - return super.visit(rs); - } - }); - } - } - } - } else { - boolean handledByLombok = false; - if (methodName.startsWith("get") && methodName.length() > 3) { - String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); - TypeDeclaration typeDecl = context.getTypeDeclaration(typeName); - if (typeDecl != null) { - String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); - int fieldIndex = findConstructorArgumentIndexForLombokField(typeDecl, fieldName); - if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) { - extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) cic.arguments().get(fieldIndex), constants); - handledByLombok = true; - } - } - } - if (!handledByLombok && methodName != null && !methodName.isEmpty()) { - // Try proper getter evaluation: builds field→value from constructor args, - // then evaluates the getter body (handles switch-based enum transformations). - java.util.List narrowed = resolveInlineInstantiationGetterResult( - cic, methodName, context, new java.util.HashSet<>()); - if (narrowed.isEmpty()) { - String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); - CompilationUnit contextCu = expr.getRoot() instanceof CompilationUnit ? (CompilationUnit) expr.getRoot() : null; - TypeDeclaration typeDecl = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName); - if (typeDecl == null) typeDecl = context.getTypeDeclaration(typeName); - if (typeDecl != null) { - narrowed = resolveMethodReturnConstant(context.getFqn(typeDecl), methodName, 0, new java.util.HashSet<>(), contextCu); - } - } - if (narrowed != null && !narrowed.isEmpty()) { - constants.addAll(narrowed); - handledByLombok = true; // reuse flag to skip fallback below - } - } - if (!handledByLombok) { - // Fallback: extract all constructor args (catches cases where getter - // evaluation is not possible, e.g. no source for the wrapper class) - for (Object argObj : cic.arguments()) { - extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants); - } - } - } - } - - // 3. Builder Pattern (e.g. MessageBuilder.withPayload(...).setHeader("event", CONSTANT).build()) - org.eclipse.jdt.core.dom.Expression currentMi = mi; - while (currentMi instanceof org.eclipse.jdt.core.dom.MethodInvocation chainMi) { - String chainName = chainMi.getName().getIdentifier(); - if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) { - extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(1), constants); - } else if ((chainName.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) { - extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(0), constants); - } - currentMi = chainMi.getExpression(); - } - - // 4. Delegate to method return analysis - TypeDeclaration td = findEnclosingType(mi); - if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression)) { - List values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new java.util.HashSet<>(), null); - if (values != null) constants.addAll(values); - } - } - } - - /** - * Builds a map of {@code fieldName → resolved-value} (and {@code this.fieldName → value}) - * by correlating the actual arguments of the given {@link ClassInstanceCreation} with the - * 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, - 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()); - TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName); - if (actualTd == null) { - actualTd = td; // Fallback to passed-in type - } - - java.util.Map fieldValues = new java.util.HashMap<>(); - java.util.Set visitedCtors = new java.util.HashSet<>(); - 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, - RhsResolver rhsResolver) { - - String tdKey = context.getFqn(td); - if (!visitedCtors.add(tdKey)) return; - - for (org.eclipse.jdt.core.dom.MethodDeclaration ctor : td.getMethods()) { - if (!ctor.isConstructor() || ctor.getBody() == null) continue; - if (ctor.parameters().size() != cic.arguments().size()) continue; - - // 1. Map each parameter name → resolved argument value - java.util.Map paramValues = new java.util.HashMap<>(); - for (int i = 0; i < ctor.parameters().size(); i++) { - String pName = ((org.eclipse.jdt.core.dom.SingleVariableDeclaration) - ctor.parameters().get(i)).getName().getIdentifier(); - org.eclipse.jdt.core.dom.Expression argExpr = - (org.eclipse.jdt.core.dom.Expression) cic.arguments().get(i); - java.util.List consts = new java.util.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; - - // 2. Scan constructor body for field assignments: this.field = param OR field = param - ctor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.Assignment node) { - org.eclipse.jdt.core.dom.Expression lhs = node.getLeftHandSide(); - org.eclipse.jdt.core.dom.Expression rhs = node.getRightHandSide(); - - String fieldName = null; - if (lhs instanceof org.eclipse.jdt.core.dom.FieldAccess fa - && fa.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression) { - fieldName = fa.getName().getIdentifier(); - } else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn - && !paramValues.containsKey(sn.getIdentifier())) { - fieldName = sn.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); - } - } - return super.visit(node); - } - }); - - // 3. Follow super constructor chain - ctor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.SuperConstructorInvocation sci) { - String superFqn = context.getSuperclassFqn(td); - if (superFqn != null) { - TypeDeclaration superTd = context.getTypeDeclaration(superFqn); - if (superTd != null) { - processSuperConstructorArgs(superTd, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver); - } - } - return super.visit(sci); - } - }); - break; // first matching constructor is enough - } - } - - private void processSuperConstructorArgs( - TypeDeclaration superTd, - List superArgs, - java.util.Map subClassParamValues, - CodebaseContext context, - java.util.Map fieldValues, - java.util.Set visitedCtors, - RhsResolver rhsResolver) { - - if (superTd == null) return; - - for (MethodDeclaration superCtor : superTd.getMethods()) { - if (!superCtor.isConstructor() || superCtor.getBody() == null) continue; - if (superCtor.parameters().size() != superArgs.size()) continue; - - // Map super() argument values to superclass constructor parameters - // superArgs may be parameter references from the subclass constructor - java.util.Map superParamValues = new java.util.HashMap<>(); - for (int i = 0; i < superCtor.parameters().size(); i++) { - String pName = ((org.eclipse.jdt.core.dom.SingleVariableDeclaration) - superCtor.parameters().get(i)).getName().getIdentifier(); - org.eclipse.jdt.core.dom.Expression argExpr = - (org.eclipse.jdt.core.dom.Expression) superArgs.get(i); - - // If the argument is a SimpleName (parameter reference), resolve it using subclass paramValues - String resolved = null; - if (argExpr instanceof org.eclipse.jdt.core.dom.SimpleName sn) { - resolved = subClassParamValues.get(sn.getIdentifier()); - } - if (resolved == null) { - java.util.List consts = new java.util.ArrayList<>(); - extractConstantsFromArgument(argExpr, consts); - if (!consts.isEmpty()) { - resolved = consts.get(0); - } else { - resolved = constantResolver.resolve(argExpr, context); - } - } - if (resolved != null) { - superParamValues.put(pName, resolved); - } - } - if (superParamValues.isEmpty()) continue; - - // Scan superclass constructor body for field assignments - superCtor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.Assignment node) { - org.eclipse.jdt.core.dom.Expression lhs = node.getLeftHandSide(); - org.eclipse.jdt.core.dom.Expression rhs = node.getRightHandSide(); - - String fieldName = null; - if (lhs instanceof org.eclipse.jdt.core.dom.FieldAccess fa - && fa.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression) { - fieldName = fa.getName().getIdentifier(); - } else if (lhs instanceof org.eclipse.jdt.core.dom.SimpleName sn - && !superParamValues.containsKey(sn.getIdentifier())) { - fieldName = sn.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); - } - } - return super.visit(node); - } - }); - - // Recursively follow further super() calls, passing down the current superclass paramValues - superCtor.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.SuperConstructorInvocation sci) { - String superFqn = context.getSuperclassFqn(superTd); - if (superFqn != null) { - TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn); - if (nextSuperTd != null) { - processSuperConstructorArgs(nextSuperTd, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver); - } - } - return super.visit(sci); - } - }); - break; // first matching super constructor is enough - } - } - - /** - * 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. - */ - protected org.eclipse.jdt.core.dom.ClassInstanceCreation findReturnedCIC( - String className, String methodName, CodebaseContext context) { - return findReturnedCIC(className, methodName, context, new java.util.HashSet<>(), 0); - } - - private org.eclipse.jdt.core.dom.ClassInstanceCreation findReturnedCIC( - String className, String methodName, CodebaseContext context, - java.util.Set visited, int depth) { - if (depth > 50 || !visited.add(className + "#" + methodName)) { - return null; - } - - org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(className); - if (td != null) { - org.eclipse.jdt.core.dom.MethodDeclaration md = - context.findMethodDeclaration(td, methodName, true); - if (md != null && md.getBody() != null) { - org.eclipse.jdt.core.dom.ClassInstanceCreation[] result = {null}; - md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) { - if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { - result[0] = cic; - } - return false; - } - }); - if (result[0] != null) return result[0]; - } - } - // Recurse into implementations (handles interfaces / abstract classes) - java.util.List impls = context.getImplementations(className); - for (String impl : impls) { - org.eclipse.jdt.core.dom.ClassInstanceCreation cic = findReturnedCIC(impl, methodName, context, visited, depth + 1); - if (cic != null) return cic; - } - return null; - } - - /** - * Resolves the value produced by calling {@code getterName} on an object created via the - * given {@link ClassInstanceCreation}. - * - *

Algorithm: - *

    - *
  1. Build a {@code fieldName → value} map from the CIC's constructor arguments.
  2. - *
  3. Ask {@link click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver} - * to evaluate the getter body with those values as pre-seeded locals.
  4. - *
- * - *

This correctly handles both simple pass-through getters ({@code return field}) and - * transformation getters that map one enum to another via a switch expression. - * - * @return resolved values, or an empty list when resolution is not possible - */ - private java.util.List resolveInlineInstantiationGetterResult( - org.eclipse.jdt.core.dom.ClassInstanceCreation cic, - String getterName, - CodebaseContext context, - java.util.Set visited) { - - String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils - .extractSimpleTypeName(cic.getType()); - org.eclipse.jdt.core.dom.CompilationUnit contextCu = cic.getRoot() instanceof org.eclipse.jdt.core.dom.CompilationUnit ? (org.eclipse.jdt.core.dom.CompilationUnit) cic.getRoot() : null; - org.eclipse.jdt.core.dom.TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName); - if (td == null) td = context.getTypeDeclaration(typeName); - if (td == null) return java.util.Collections.emptyList(); - - org.eclipse.jdt.core.dom.MethodDeclaration getter = - context.findMethodDeclaration(td, getterName, true); - if (getter == null || getter.getBody() == null) return java.util.Collections.emptyList(); - - String getterKey = typeName + "#" + getterName; - if (!visited.add(getterKey)) { - return java.util.Collections.emptyList(); // Cycle detected - } - - try { - java.util.Map fieldValues = buildFieldValuesFromCIC(td, cic, context); - if (fieldValues.isEmpty()) return java.util.Collections.emptyList(); - - String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited); - return result != null ? java.util.List.of(result) : java.util.Collections.emptyList(); - } finally { - visited.remove(getterKey); - } - } - - private int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) { - boolean isAllArgsConstructor = false; - boolean isRequiredArgsConstructor = false; - boolean isData = false; - boolean isValue = false; - - for (Object modObj : td.modifiers()) { - if (modObj instanceof org.eclipse.jdt.core.dom.MarkerAnnotation ma) { - String name = ma.getTypeName().getFullyQualifiedName(); - if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true; - if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true; - if (name.equals("Data") || name.equals("lombok.Data")) isData = true; - if (name.equals("Value") || name.equals("lombok.Value")) isValue = true; - } else if (modObj instanceof org.eclipse.jdt.core.dom.NormalAnnotation na) { - String name = na.getTypeName().getFullyQualifiedName(); - if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true; - if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true; - if (name.equals("Data") || name.equals("lombok.Data")) isData = true; - if (name.equals("Value") || name.equals("lombok.Value")) isValue = true; - } - } - - if (!isAllArgsConstructor && !isRequiredArgsConstructor && !isData && !isValue) { - return -1; - } - - int index = 0; - for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) { - boolean isStatic = false; - boolean isFinal = false; - boolean hasNonNull = false; - for (Object modObj : fd.modifiers()) { - if (modObj instanceof org.eclipse.jdt.core.dom.Modifier mod) { - if (mod.isStatic()) isStatic = true; - if (mod.isFinal()) isFinal = true; - } else if (modObj instanceof org.eclipse.jdt.core.dom.MarkerAnnotation ma) { - if (ma.getTypeName().getFullyQualifiedName().endsWith("NonNull")) hasNonNull = true; - } - } - if (isStatic) continue; - - if (isRequiredArgsConstructor && !isAllArgsConstructor && !isFinal && !hasNonNull) continue; - if (isData && !isAllArgsConstructor && !isFinal && !hasNonNull) continue; - - for (Object fragObj : fd.fragments()) { - org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj; - if (frag.getName().getIdentifier().equals(fieldName)) { - return index; - } - index++; - } - } - return -1; - } - - protected void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List 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)) { - extractConstantsFromExpression(argObj, constants); - } - } else { - extractConstantsFromExpression(argObj, constants); - } - } - - protected org.eclipse.jdt.core.dom.Block findEnclosingBlock(org.eclipse.jdt.core.dom.ASTNode node) { - org.eclipse.jdt.core.dom.ASTNode parent = node.getParent(); - while (parent != null && !(parent instanceof org.eclipse.jdt.core.dom.Block)) { - parent = parent.getParent(); - } - return (org.eclipse.jdt.core.dom.Block) parent; - } - - protected String traceLocalVariable(String methodFqn, String varName) { - 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) { - MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md != null && md.getBody() != null) { - List initializers = new ArrayList<>(); - md.getBody().accept(new ASTVisitor() { - @Override - public boolean visit(VariableDeclarationFragment node) { - if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { - initializers.add(node.getInitializer()); - } - return super.visit(node); - } - @Override - public boolean visit(Assignment node) { - if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { - initializers.add(node.getRightHandSide()); - } - return super.visit(node); - } - }); - - if (!initializers.isEmpty()) { - List stringified = new ArrayList<>(); - for (Expression expr : initializers) { - Expression traced = traceVariable(expr); - if (traced instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { - Expression innerMost = unwrapMethodInvocation(mi, 0); - if (innerMost instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) { - if (innerMi.getExpression() instanceof SimpleName sn) { - stringified.add(sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()"); - } else { - stringified.add(innerMi.getName().getIdentifier() + "()"); - } - } else if (innerMost instanceof SimpleName sn) { - stringified.add(sn.getIdentifier()); - } else { - stringified.add(innerMost.toString()); - } - } else if (traced instanceof SimpleName sn) { - stringified.add(sn.getIdentifier()); - } else if (traced instanceof org.eclipse.jdt.core.dom.ArrayInitializer) { - stringified.add("new Object[]" + traced.toString()); - } else { - stringified.add(traced.toString()); - } - } - if (stringified.size() == 1) return stringified.get(0); - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < stringified.size() - 1; i++) { - sb.append("true ? ").append(stringified.get(i)).append(" : "); - } - sb.append(stringified.get(stringified.size() - 1)); - return sb.toString(); - } - } - - // If local variable not found, check field assignments - String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName; - final Expression[] fieldInitializer = new Expression[1]; - ASTVisitor fieldVisitor = new ASTVisitor() { - @Override - public boolean visit(VariableDeclarationFragment node) { - if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) { - fieldInitializer[0] = node.getInitializer(); - } - return super.visit(node); - } - @Override - public boolean visit(Assignment node) { - Expression left = node.getLeftHandSide(); - if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) || - (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) { - fieldInitializer[0] = node.getRightHandSide(); - } - return super.visit(node); - } - }; - - for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) { - for (Object fragObj : fd.fragments()) { - VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; - if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) { - fieldInitializer[0] = frag.getInitializer(); - } - } - } - if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); - - for (MethodDeclaration classMd : td.getMethods()) { - if (classMd.isConstructor() && classMd.getBody() != null) { - classMd.getBody().accept(fieldVisitor); - } - } - if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); - - for (MethodDeclaration classMd : td.getMethods()) { - if (!classMd.isConstructor() && classMd.getBody() != null) { - classMd.getBody().accept(fieldVisitor); - } - } - if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); - } - return null; - } - - protected String traceLocalVariable(String methodFqn, String varName, java.util.Map parameterValues) { - String result = traceLocalVariable(methodFqn, varName); - if (result != null && parameterValues != null && !parameterValues.isEmpty()) { - TypeDeclaration td = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))); - if (td != null) { - String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); - MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md != null && md.getBody() != null) { - final org.eclipse.jdt.core.dom.ASTNode[] switchNode = new org.eclipse.jdt.core.dom.ASTNode[1]; - md.getBody().accept(new ASTVisitor() { - @Override - public boolean visit(VariableDeclarationFragment node) { - if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { - Expression init = node.getInitializer(); - if (init.getNodeType() == ASTNode.SWITCH_EXPRESSION) { - switchNode[0] = init; - return false; - } - if (init.getNodeType() == ASTNode.SWITCH_STATEMENT) { - switchNode[0] = init; - return false; - } - } - return super.visit(node); - } - @Override - public boolean visit(Assignment node) { - if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { - Expression rhs = node.getRightHandSide(); - if (rhs.getNodeType() == ASTNode.SWITCH_EXPRESSION) { - switchNode[0] = rhs; - return false; - } - if (rhs.getNodeType() == ASTNode.SWITCH_STATEMENT) { - switchNode[0] = rhs; - return false; - } - } - return super.visit(node); - } - }); - if (switchNode[0] != null) { - if (switchNode[0] instanceof SwitchExpression se) { - String evaluated = constantResolver.evaluateSwitchWithParams(se, parameterValues, context); - if (evaluated != null) return evaluated; - } else if (switchNode[0] instanceof SwitchStatement ss) { - String evaluated = constantResolver.evaluateSwitchWithParams(ss, parameterValues, context); - if (evaluated != null) return evaluated; - } - } - } - } - } - return result; - } - - protected java.util.Map buildParameterValuesMap(String caller, String target, Map> callGraph, List path, int pathIndex) { - java.util.Map paramValues = new java.util.HashMap<>(); - List edges = callGraph.get(caller); - if (edges == null) { - return paramValues; - } - - for (CallEdge edge : edges) { - if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { - List args = edge.getArguments(); - if (args == null || args.isEmpty()) break; - - int lastDot = target.lastIndexOf('.'); - if (lastDot < 0) break; - String className = target.substring(0, lastDot); - String methodName = target.substring(lastDot + 1); - - TypeDeclaration td = context.getTypeDeclaration(className); - if (td == null) break; - - MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md == null) break; - - List paramNames = new java.util.ArrayList<>(); - for (Object paramObj : md.parameters()) { - org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) paramObj; - paramNames.add(param.getName().getIdentifier()); - } - - int minSize = java.lang.Math.min(paramNames.size(), args.size()); - for (int j = 0; j < minSize; j++) { - String argValue = args.get(j); - String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph); - paramValues.put(paramNames.get(j), resolvedArgValue); - } - break; - } - } - return paramValues; - } - - protected String resolveArgumentValue(String argValue, String callerMethod, List path, int pathIndex, Map> callGraph) { - if (argValue == null) return null; - if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*")) { - return argValue; - } - - int callerIndex = path.indexOf(callerMethod); - if (callerIndex <= 0) return argValue; - - String prevCaller = path.get(callerIndex - 1); - List prevEdges = callGraph.get(prevCaller); - if (prevEdges == null) return argValue; - - for (CallEdge edge : prevEdges) { - if (edge.getTargetMethod().equals(callerMethod) || isHeuristicMatch(edge.getTargetMethod(), callerMethod)) { - List prevArgs = edge.getArguments(); - if (prevArgs == null || prevArgs.isEmpty()) break; - - int lastDot = callerMethod.lastIndexOf('.'); - if (lastDot < 0) break; - String className = callerMethod.substring(0, lastDot); - String methodName = callerMethod.substring(lastDot + 1); - - TypeDeclaration td = context.getTypeDeclaration(className); - if (td == null) break; - - MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md == null) break; - - List paramNames = new java.util.ArrayList<>(); - for (Object paramObj : md.parameters()) { - org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) paramObj; - paramNames.add(param.getName().getIdentifier()); - } - - int paramIdx = paramNames.indexOf(argValue); - if (paramIdx >= 0 && paramIdx < prevArgs.size()) { - String prevArg = prevArgs.get(paramIdx); - return resolveArgumentValue(prevArg, prevCaller, path, pathIndex, callGraph); - } - break; - } - } - return argValue; - } - - protected org.eclipse.jdt.core.dom.Expression traceLocalSetter(String methodFqn, String varName, String getterName) { - if (methodFqn == null || !methodFqn.contains(".")) return null; - String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); - String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); - org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(className); - if (td != null) { - org.eclipse.jdt.core.dom.MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md != null && md.getBody() != null) { - final org.eclipse.jdt.core.dom.Expression[] setterArg = new org.eclipse.jdt.core.dom.Expression[1]; - String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName; - md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.MethodInvocation node) { - if (node.getName().getIdentifier().equalsIgnoreCase("set" + propName) || node.getName().getIdentifier().equalsIgnoreCase(propName)) { - if (node.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName sn && sn.getIdentifier().equals(varName)) { - if (!node.arguments().isEmpty()) { - setterArg[0] = (org.eclipse.jdt.core.dom.Expression) node.arguments().get(0); - } - } - } - return super.visit(node); - } - @Override - public boolean visit(org.eclipse.jdt.core.dom.Assignment node) { - if (node.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { - if (fa.getExpression() instanceof org.eclipse.jdt.core.dom.SimpleName faSn && faSn.getIdentifier().equals(varName)) { - if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) { - setterArg[0] = node.getRightHandSide(); - } - } - } else if (node.getLeftHandSide() instanceof org.eclipse.jdt.core.dom.QualifiedName qqn) { - if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) { - if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) { - setterArg[0] = node.getRightHandSide(); - } - } - } - return super.visit(node); - } - }); - return setterArg[0]; - } - } - return null; - } - - protected Expression unwrapMethodInvocation(org.eclipse.jdt.core.dom.MethodInvocation mi, int depth) { - if (depth > 5) return mi; - // Do not unwrap if called on an object (e.g. mapper.toEvent) - // Exceptions: Optional.of, Optional.ofNullable, Stream, etc. - if (mi.getExpression() != null) { - String exprStr = mi.getExpression().toString(); - if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) { - return mi; - } - } - - if (!mi.arguments().isEmpty()) { - String mName = mi.getName().getIdentifier(); - String lowerName = mName.toLowerCase(); - - // Do not unwrap methods that explicitly convert, map, parse, or create - if (lowerName.startsWith("map") || lowerName.startsWith("parse") || - lowerName.startsWith("convert") || lowerName.startsWith("to") || - lowerName.startsWith("resolve") || lowerName.startsWith("build") || - lowerName.startsWith("create") || lowerName.startsWith("from") || - lowerName.startsWith("transform") || lowerName.startsWith("translate") || - lowerName.startsWith("derive") || lowerName.startsWith("determine") || - lowerName.startsWith("calculate") || lowerName.startsWith("decode") || - lowerName.startsWith("extract") || lowerName.startsWith("get") || - lowerName.startsWith("find")) { - return mi; - } - - Expression arg = (Expression) mi.arguments().get(0); - if (arg instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) { - return unwrapMethodInvocation(innerMi, depth + 1); - } - return arg; - } - return mi; - } - - protected String extractContextMachineId(List path, Map> callGraph) { - for (String node : path) { - List edges = callGraph.get(node); - if (edges != null) { - for (CallEdge edge : edges) { - String target = edge.getTargetMethod(); - if (target != null && (target.contains(".restore") || target.contains(".read"))) { - // Persister signatures usually like: restore(stateMachine, contextObj) - if (edge.getArguments().size() >= 2) { - return edge.getArguments().get(1); // The contextObj / machineId - } - } - } - } - } - return null; - } - protected MethodDeclaration findEnclosingMethod(ASTNode node) { ASTNode parent = node.getParent(); while (parent != null && !(parent instanceof MethodDeclaration)) { @@ -1974,11 +596,236 @@ import java.util.*; return (MethodDeclaration) parent; } - protected ASTNode findStatement(ASTNode node) { - while (node != null && !(node instanceof Statement)) { - node = node.getParent(); + protected TypeDeclaration findEnclosingType(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof TypeDeclaration)) { + parent = parent.getParent(); } - return node; + return (TypeDeclaration) parent; + } + + protected List resolveArguments(List astArguments) { + List args = new ArrayList<>(); + for (Object argObj : astArguments) { + args.add(resolveArgument((Expression) argObj)); + } + return args; + } + + protected String resolveArgument(Expression expr) { + if (expr instanceof LambdaExpression le) { + ASTNode body = le.getBody(); + if (body instanceof Expression bodyExpr) { + expr = bodyExpr; + } else if (body instanceof Block block) { + for (Object stmtObj : block.statements()) { + if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) { + expr = rs.getExpression(); + break; + } + } + } + } + + if (expr instanceof ExpressionMethodReference emr) { + TypeDeclaration td = findEnclosingType(emr); + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true); + if (md != null && md.getBody() != null) { + for (Object stmtObj : md.getBody().statements()) { + if (stmtObj instanceof ReturnStatement rs && rs.getExpression() != null) { + expr = rs.getExpression(); + break; + } + } + } + } + } + + if (expr instanceof ClassInstanceCreation cic && !cic.arguments().isEmpty()) { + Expression firstArg = (Expression) cic.arguments().get(0); + String resolved = constantResolver.resolve(firstArg, context); + if (resolved != null) { + expr = firstArg; + } + } + + String val = constantResolver.resolve(expr, context); + if (val == null) { + val = expr.toString(); + } + + return postProcessResolvedArgument(expr, val); + } + + protected String postProcessResolvedArgument(Expression originalExpr, String resolvedValue) { + return resolvedValue; + } + + protected String resolveMethodInType(TypeDeclaration td, String methodName) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null) { + TypeDeclaration declaringTd = findEnclosingType(md); + return context.getFqn(declaringTd) + "." + methodName; + } + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + TypeDeclaration targetTd = context.resolveStaticImport(methodName, cu); + if (targetTd != null) { + return context.getFqn(targetTd) + "." + methodName; + } + return null; + } + + protected Expression traceVariable(Expression expr) { + return variableTracer.traceVariable(expr); + } + + protected Map buildFieldValuesFromCIC( + TypeDeclaration td, + ClassInstanceCreation cic, + CodebaseContext context, + RhsResolver rhsResolver) { + return constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, (rhs, paramValues, typeDecl) -> rhsResolver.resolve(rhs, paramValues, typeDecl)); + } + + protected Map buildFieldValuesFromCIC( + TypeDeclaration td, + ClassInstanceCreation cic, + CodebaseContext context) { + return constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context); + } + + protected String evaluateMethodCallInConstructor( + Expression rhs, + Map paramValues, + TypeDeclaration td) { + return constructorAnalyzer.evaluateMethodCallInConstructor(rhs, paramValues, td); + } + + protected ClassInstanceCreation findReturnedCIC( + String className, String methodName, CodebaseContext context) { + return constructorAnalyzer.findReturnedCIC(className, methodName, context); + } + + protected List evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List path, Map> callGraph) { + ASTParser exprParser = ASTParser.newParser(AST.JLS17); + exprParser.setKind(ASTParser.K_EXPRESSION); + exprParser.setSource(resolvedValue.toCharArray()); + ASTNode exprNode = exprParser.createAST(null); + + if (exprNode instanceof MethodInvocation mi) { + String getterName = mi.getName().getIdentifier(); + Expression receiver = mi.getExpression(); + String receiverName = null; + String receiverType = null; + + if (receiver instanceof SimpleName sn) { + receiverName = sn.getIdentifier(); + receiverType = variableTracer.getVariableDeclaredType(entryMethod, receiverName); + } + + if (receiverType == null && path.size() > 1) { + String callerMethod = path.get(1); + receiverType = variableTracer.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; + Expression initExpr = findVariableInitializer(entryMethod, varName); + if (initExpr instanceof MethodInvocation initMi) { + initExpr = unwrapMethodInvocation(initMi, 0); + } + if (!(initExpr instanceof 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(constructorAnalyzer.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 + } + } + } + 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 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 Expression[] initExpr = {null}; + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationStatement node) { + for (Object fragObj : node.fragments()) { + VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(varName) && frag.getInitializer() != null) { + initExpr[0] = frag.getInitializer(); + return false; + } + } + return super.visit(node); + } + }); + return initExpr[0]; } protected String[] extractMethodSuffix(String paramName, String currentSuffix) { @@ -2002,669 +849,44 @@ import java.util.*; return new String[] { paramName, currentSuffix }; } - protected String parseEnumSetElement(String eVal) { - return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal; - } - - /** - * Resolves a single argument expression to a string value. - * Handles lambdas, method references, and constructor argument unwrapping. - * Subclasses can override {@link #postProcessResolvedArgument(Expression, String)} for custom behavior. - */ - protected String resolveArgument(org.eclipse.jdt.core.dom.Expression expr) { - // Extract from lambda - if (expr instanceof org.eclipse.jdt.core.dom.LambdaExpression le) { - org.eclipse.jdt.core.dom.ASTNode body = le.getBody(); - if (body instanceof org.eclipse.jdt.core.dom.Expression bodyExpr) { - expr = bodyExpr; - } else if (body instanceof org.eclipse.jdt.core.dom.Block block) { - for (Object stmtObj : block.statements()) { - if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs && rs.getExpression() != null) { - expr = rs.getExpression(); - break; - } - } + protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) { + if (depth > 5) return mi; + if (mi.getExpression() != null) { + String exprStr = mi.getExpression().toString(); + if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) { + return mi; } } - - // Extract from method reference - if (expr instanceof org.eclipse.jdt.core.dom.ExpressionMethodReference emr) { - org.eclipse.jdt.core.dom.TypeDeclaration td = findEnclosingType(emr); - if (td != null) { - org.eclipse.jdt.core.dom.MethodDeclaration md = context.findMethodDeclaration(td, emr.getName().getIdentifier(), true); - if (md != null && md.getBody() != null) { - for (Object stmtObj : md.getBody().statements()) { - if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs && rs.getExpression() != null) { - expr = rs.getExpression(); - break; - } - } - } + + if (!mi.arguments().isEmpty()) { + String mName = mi.getName().getIdentifier(); + String lowerName = mName.toLowerCase(); + + if (lowerName.startsWith("map") || lowerName.startsWith("parse") || + lowerName.startsWith("convert") || lowerName.startsWith("to") || + lowerName.startsWith("resolve") || lowerName.startsWith("build") || + lowerName.startsWith("create") || lowerName.startsWith("from") || + lowerName.startsWith("transform") || lowerName.startsWith("translate") || + lowerName.startsWith("derive") || lowerName.startsWith("determine") || + lowerName.startsWith("calculate") || lowerName.startsWith("decode") || + lowerName.startsWith("extract") || lowerName.startsWith("get") || + lowerName.startsWith("find")) { + return mi; } - } - - // Unwrap constructor first argument if it's a constant (e.g., new CustomMessage(OrderEvent.PROCESS)) - if (expr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic && !cic.arguments().isEmpty()) { - org.eclipse.jdt.core.dom.Expression firstArg = (org.eclipse.jdt.core.dom.Expression) cic.arguments().get(0); - String resolved = constantResolver.resolve(firstArg, context); - if (resolved != null) { - expr = firstArg; + + Expression arg = (Expression) mi.arguments().get(0); + if (arg instanceof MethodInvocation innerMi) { + return unwrapMethodInvocation(innerMi, depth + 1); } + return arg; } - - String val = constantResolver.resolve(expr, context); - if (val == null) { - val = expr.toString(); // Fallback to raw string (handles Enums, 'new Class()', etc) - } - - // Allow subclasses to post-process (e.g., preserve getters for later substitution) - return postProcessResolvedArgument(expr, val); + return mi; } - /** - * Hook for subclasses to customize argument resolution. - * Default implementation returns the value as-is. - */ - protected String postProcessResolvedArgument(org.eclipse.jdt.core.dom.Expression originalExpr, String resolvedValue) { - return resolvedValue; - } - - /** - * Resolves a list of argument expressions. - * Default implementation calls {@link #resolveArgument(Expression)} for each. - */ - protected List resolveArguments(List astArguments) { - List args = new ArrayList<>(); - for (Object argObj : astArguments) { - args.add(resolveArgument((org.eclipse.jdt.core.dom.Expression) argObj)); - } - return args; + public List resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) { + return constantExtractor.resolveClassConstantReturns(className, context, contextCu); } protected abstract Map> buildCallGraph(); protected abstract String resolveCalledMethod(MethodInvocation node); - protected List findPath(String start, String target, Map> graph, Set visited) { - if (start.equals(target)) return new ArrayList<>(List.of(start)); - if (!visited.add(start)) return null; // Path-scoped cycle detection - - List neighbors = graph.get(start); - if (neighbors != null) { - for (CallEdge edge : neighbors) { - String neighbor = edge.getTargetMethod(); - if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) { - visited.remove(start); - return new ArrayList<>(List.of(start, target)); - } - List path = findPath(neighbor, target, graph, visited); - if (path != null) { - path.add(0, start); - visited.remove(start); - return path; - } - } - } - - if (log.isDebugEnabled()) { - log.debug("Path search dead-end at {} when looking for {}", start, target); - } - - visited.remove(start); - return null; - } - - private boolean isFullyQualifiedMethod(String methodFqn) { - if (methodFqn == null || !methodFqn.contains(".")) return false; - int lastDot = methodFqn.lastIndexOf('.'); - String classPart = methodFqn.substring(0, lastDot); - if (classPart.contains(".")) return true; - if (!classPart.isEmpty() && Character.isUpperCase(classPart.charAt(0))) return true; - return false; - } - - protected boolean isHeuristicMatch(String neighbor, String target) { - if (neighbor == null || target == null) return false; - if (neighbor.equals(target)) return true; - - // If both have packages/classes, they must match more strictly - if (isFullyQualifiedMethod(neighbor) && isFullyQualifiedMethod(target)) { - int lastDotNeighbor = neighbor.lastIndexOf('.'); - int lastDotTarget = target.lastIndexOf('.'); - if (lastDotNeighbor < 0 || lastDotTarget < 0) return false; - - String methodNeighbor = neighbor.substring(lastDotNeighbor + 1); - String methodTarget = target.substring(lastDotTarget + 1); - - if (!methodNeighbor.equals(methodTarget)) { - return false; - } - - String classNeighbor = neighbor.substring(0, lastDotNeighbor); - String classTarget = target.substring(0, lastDotTarget); - - if (classNeighbor.equals(classTarget)) return true; - - String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor; - String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget; - - if (simpleClassNeighbor.equals(simpleClassTarget)) return true; - if (simpleClassNeighbor.contains(simpleClassTarget) || simpleClassTarget.contains(simpleClassNeighbor)) return true; - - // Check context implementations - List impls = context.getImplementations(classTarget); - if (impls != null) { - for (String impl : impls) { - if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true; - } - } - List implsN = context.getImplementations(classNeighbor); - if (implsN != null) { - for (String impl : implsN) { - if (impl.equals(classTarget) || impl.endsWith("." + classTarget)) return true; - } - } - - return false; - } - - // Fallback for simple names (only if one side is unqualified) - String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target; - String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor; - return simpleNeighbor.equals(simpleTarget); - } - - protected TypeDeclaration findEnclosingType(ASTNode node) { - ASTNode parent = node.getParent(); - while (parent != null && !(parent instanceof TypeDeclaration)) { - parent = parent.getParent(); - } - return (TypeDeclaration) parent; - } - protected Expression traceVariable(Expression expr) { - List all = traceVariableAll(expr, new HashSet<>()); - return all.isEmpty() ? expr : all.get(all.size() - 1); - } - - protected List traceVariableAll(Expression expr) { - return traceVariableAll(expr, new HashSet<>()); - } - - protected List traceVariableAll(Expression expr, Set visitedVariables) { - List results = new ArrayList<>(); - if (expr instanceof SimpleName sn) { - String varName = sn.getIdentifier(); - String methodKey = ""; - MethodDeclaration enclosingMethod = findEnclosingMethod(expr); - if (enclosingMethod != null) { - TypeDeclaration enclosingType = findEnclosingType(enclosingMethod); - if (enclosingType != null) { - methodKey = context.getFqn(enclosingType) + "." + enclosingMethod.getName().getIdentifier() + "#" + varName; - } - } - if (methodKey.isEmpty()) { - methodKey = varName; - } - if (!visitedVariables.add(methodKey)) { - return results; // Break infinite loop - } - if (enclosingMethod != null) { - enclosingMethod.accept(new ASTVisitor() { - @Override - public boolean visit(VariableDeclarationFragment node) { - if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { - results.addAll(traceVariableAll(node.getInitializer(), visitedVariables)); - } - return super.visit(node); - } - @Override - public boolean visit(Assignment node) { - if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { - results.addAll(traceVariableAll(node.getRightHandSide(), visitedVariables)); - } - return super.visit(node); - } - }); - if (!results.isEmpty()) { - visitedVariables.remove(methodKey); - return results; - } - - // Tracing parameter callers (Inter-procedural within the same class) - for (Object paramObj : enclosingMethod.parameters()) { - org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) paramObj; - if (param.getName().getIdentifier().equals(varName)) { - int paramIndex = enclosingMethod.parameters().indexOf(param); - TypeDeclaration enclosingType = findEnclosingType(enclosingMethod); - if (enclosingType != null) { - String mName = enclosingMethod.getName().getIdentifier(); - List callerExprs = new ArrayList<>(); - enclosingType.accept(new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(org.eclipse.jdt.core.dom.MethodInvocation node) { - if (node.getName().getIdentifier().equals(mName) && node.arguments().size() > paramIndex) { - callerExprs.addAll(traceVariableAll((Expression) node.arguments().get(paramIndex), visitedVariables)); - } - return super.visit(node); - } - @Override - public boolean visit(org.eclipse.jdt.core.dom.ExpressionMethodReference node) { - if (node.getName().getIdentifier().equals(mName)) { - if (node.getParent() instanceof org.eclipse.jdt.core.dom.MethodInvocation parentMi) { - if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(parentMi)) { - callerExprs.addAll(traceVariableAll(parentMi.getExpression(), visitedVariables)); - } else { - for (Object arg : parentMi.arguments()) { - if (arg != node) { - callerExprs.addAll(traceVariableAll((Expression) arg, visitedVariables)); - } - } - } - } - } - return super.visit(node); - } - }); - if (!callerExprs.isEmpty()) { - visitedVariables.remove(methodKey); - return callerExprs; - } - } - } - } - } - visitedVariables.remove(methodKey); - } - results.add(expr); - return results; - } - - protected List traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set visited) { - List results = new ArrayList<>(); - - // 1. Check Field Declarations - for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) { - for (Object fragObj : fd.fragments()) { - org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj; - if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) { - Expression right = frag.getInitializer(); - List tracedRights = traceVariableAll(right); - for (Expression tracedRight : tracedRights) { - if (tracedRight instanceof MethodInvocation mi) { - String calledName = mi.getName().getIdentifier(); - String targetClass = context.getFqn(td); - CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; - if (mi.getExpression() == null) { - TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu); - if (targetTd != null) { - targetClass = context.getFqn(targetTd); - } - } - results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu)); - } else { - String val = constantResolver.resolve(tracedRight, context); - if (val != null) results.add(val); - } - } - } - } - } - - org.eclipse.jdt.core.dom.ASTVisitor assignmentVisitor = new org.eclipse.jdt.core.dom.ASTVisitor() { - @Override - public boolean visit(Assignment node) { - Expression left = node.getLeftHandSide(); - if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) || - (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) { - - Expression right = node.getRightHandSide(); - List tracedRights = traceVariableAll(right); - for (Expression tracedRight : tracedRights) { - if (tracedRight instanceof MethodInvocation mi) { - String calledName = mi.getName().getIdentifier(); - String targetClass = context.getFqn(td); - - if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) { - targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName(); - } else if (mi.getExpression() instanceof SimpleName) { - String exprName = ((SimpleName) mi.getExpression()).getIdentifier(); - CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; - TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu); - if (targetTd != null) { - targetClass = context.getFqn(targetTd); - } - } else if (mi.getExpression() == null) { - CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; - TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu); - if (targetTd != null) { - targetClass = context.getFqn(targetTd); - } - } - - CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; - results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu)); - } else { - String val = constantResolver.resolve(tracedRight, context); - if (val != null) results.add(val); - } - } - } - return super.visit(node); - } - @Override - public boolean visit(ConstructorInvocation node) { - processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited); - return super.visit(node); - } - - @Override - public boolean visit(SuperConstructorInvocation node) { - TypeDeclaration enclosingTd = findEnclosingType(node); - if (enclosingTd != null) { - String superFqn = context.getSuperclassFqn(enclosingTd); - if (superFqn != null) { - TypeDeclaration superTd = context.getTypeDeclaration(superFqn); - processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited); - } - } - return super.visit(node); - } - }; - - // 2. Check Constructors - for (MethodDeclaration md : td.getMethods()) { - if (md.isConstructor() && md.getBody() != null) { - md.getBody().accept(assignmentVisitor); - } - } - - // 3. Check Initializers - for (Object bodyDecl : td.bodyDeclarations()) { - if (bodyDecl instanceof org.eclipse.jdt.core.dom.Initializer init && init.getBody() != null) { - init.getBody().accept(assignmentVisitor); - } - } - - return results; - } - - protected void processConstructorInvocationArgs(List arguments, TypeDeclaration targetTd, ASTNode callNode, List results, String fieldName, CodebaseContext context, Set visited) { - if (targetTd == null) return; - MethodDeclaration callerMd = findEnclosingMethod(callNode); - - org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = null; - if (callNode instanceof ConstructorInvocation ci) { - resolvedBinding = ci.resolveConstructorBinding(); - } else if (callNode instanceof SuperConstructorInvocation sci) { - resolvedBinding = sci.resolveConstructorBinding(); - } - - for (MethodDeclaration otherMd : targetTd.getMethods()) { - boolean matches = false; - if (resolvedBinding != null && otherMd.resolveBinding() != null) { - matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey()); - } else { - matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd; - } - - if (matches) { - int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new java.util.HashSet<>()); - if (targetIdx >= 0 && targetIdx < arguments.size()) { - Expression arg = (Expression) arguments.get(targetIdx); - String val = constantResolver.resolve(arg, context); - if (val != null) { - if (val.startsWith("ENUM_SET:")) { - for (String eVal : val.substring(9).split(",")) { - results.add(parseEnumSetElement(eVal)); - } - } else { - results.add(val); - } - } - } - } - } - } - - protected int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, java.util.Set visited) { - if (constructorMd == null || constructorMd.getBody() == null) return -1; - if (!visited.add(constructorMd)) return -1; - - final int[] foundIdx = {-1}; - constructorMd.getBody().accept(new ASTVisitor() { - @Override - public boolean visit(Assignment asn) { - Expression left = asn.getLeftHandSide(); - if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) || - (left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) || - (left instanceof SuperFieldAccess sfa && sfa.getName().getIdentifier().equals(fieldName))) { - Expression right = asn.getRightHandSide(); - String rightName = extractVariableName(right); - if (rightName != null) { - for (int i = 0; i < constructorMd.parameters().size(); i++) { - SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); - if (svd.getName().getIdentifier().equals(rightName)) { - foundIdx[0] = i; - } - } - } - } - return super.visit(asn); - } - @Override - public boolean visit(ConstructorInvocation node) { - org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = node.resolveConstructorBinding(); - TypeDeclaration enclosingTd = findEnclosingType(node); - if (enclosingTd != null) { - for (MethodDeclaration otherMd : enclosingTd.getMethods()) { - boolean matches = false; - if (resolvedBinding != null && otherMd.resolveBinding() != null) { - matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey()); - } else { - matches = otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size(); - } - if (matches) { - int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited); - if (targetIdx >= 0 && targetIdx < node.arguments().size()) { - Expression arg = (Expression) node.arguments().get(targetIdx); - String argName = extractVariableName(arg); - if (argName != null) { - for (int i = 0; i < constructorMd.parameters().size(); i++) { - SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); - if (svd.getName().getIdentifier().equals(argName)) { - foundIdx[0] = i; - } - } - } - } - } - } - } - return super.visit(node); - } - @Override - public boolean visit(SuperConstructorInvocation node) { - org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = node.resolveConstructorBinding(); - TypeDeclaration enclosingTd = findEnclosingType(node); - if (enclosingTd != null) { - String superFqn = context.getSuperclassFqn(enclosingTd); - if (superFqn != null) { - TypeDeclaration superTd = context.getTypeDeclaration(superFqn); - if (superTd != null) { - for (MethodDeclaration otherMd : superTd.getMethods()) { - boolean matches = false; - if (resolvedBinding != null && otherMd.resolveBinding() != null) { - matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey()); - } else { - matches = otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size(); - } - if (matches) { - int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited); - if (targetIdx >= 0 && targetIdx < node.arguments().size()) { - Expression arg = (Expression) node.arguments().get(targetIdx); - String argName = extractVariableName(arg); - if (argName != null) { - for (int i = 0; i < constructorMd.parameters().size(); i++) { - SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); - if (svd.getName().getIdentifier().equals(argName)) { - foundIdx[0] = i; - } - } - } - } - } - } - } - } - } - return super.visit(node); - } - }); - return foundIdx[0]; - } - - protected String extractVariableName(Expression expr) { - if (expr instanceof SimpleName sn) return sn.getIdentifier(); - if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier(); - if (expr instanceof SuperFieldAccess sfa) return sfa.getName().getIdentifier(); - if (expr instanceof CastExpression ce) return extractVariableName(ce.getExpression()); - if (expr instanceof ParenthesizedExpression pe) return extractVariableName(pe.getExpression()); - if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) { - if (!mi.arguments().isEmpty()) return extractVariableName((Expression)mi.arguments().get(0)); - } - 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]; - } - - protected String resolveMethodInType(TypeDeclaration td, String methodName) { - MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); - if (md != null) { - TypeDeclaration declaringTd = findEnclosingType(md); - return context.getFqn(declaringTd) + "." + methodName; - } - CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; - TypeDeclaration targetTd = context.resolveStaticImport(methodName, cu); - if (targetTd != null) { - return context.getFqn(targetTd) + "." + methodName; - } - return null; - } } \ No newline at end of file diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java new file mode 100644 index 0000000..874abf3 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java @@ -0,0 +1,118 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallEdge; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.extern.slf4j.Slf4j; +import java.util.*; + +@Slf4j +public class CallGraphPathFinder { + private final CodebaseContext context; + + public CallGraphPathFinder(CodebaseContext context) { + this.context = context; + } + + public List findPath(String start, String target, Map> graph, Set visited) { + if (start.equals(target)) return new ArrayList<>(List.of(start)); + if (!visited.add(start)) return null; // Path-scoped cycle detection + + List neighbors = graph.get(start); + if (neighbors != null) { + for (CallEdge edge : neighbors) { + String neighbor = edge.getTargetMethod(); + if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) { + visited.remove(start); + return new ArrayList<>(List.of(start, target)); + } + List path = findPath(neighbor, target, graph, visited); + if (path != null) { + path.add(0, start); + visited.remove(start); + return path; + } + } + } + + if (log.isDebugEnabled()) { + log.debug("Path search dead-end at {} when looking for {}", start, target); + } + + visited.remove(start); + return null; + } + + public String extractContextMachineId(List path, Map> callGraph) { + for (String node : path) { + List edges = callGraph.get(node); + if (edges != null) { + for (CallEdge edge : edges) { + String target = edge.getTargetMethod(); + if (target != null && (target.contains(".restore") || target.contains(".read"))) { + if (edge.getArguments().size() >= 2) { + return edge.getArguments().get(1); // The contextObj / machineId + } + } + } + } + } + return null; + } + + public boolean isHeuristicMatch(String neighbor, String target) { + if (neighbor == null || target == null) return false; + if (neighbor.equals(target)) return true; + + if (isFullyQualifiedMethod(neighbor) && isFullyQualifiedMethod(target)) { + int lastDotNeighbor = neighbor.lastIndexOf('.'); + int lastDotTarget = target.lastIndexOf('.'); + if (lastDotNeighbor < 0 || lastDotTarget < 0) return false; + + String methodNeighbor = neighbor.substring(lastDotNeighbor + 1); + String methodTarget = target.substring(lastDotTarget + 1); + + if (!methodNeighbor.equals(methodTarget)) { + return false; + } + + String classNeighbor = neighbor.substring(0, lastDotNeighbor); + String classTarget = target.substring(0, lastDotTarget); + + if (classNeighbor.equals(classTarget)) return true; + + String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor; + String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget; + + if (simpleClassNeighbor.equals(simpleClassTarget)) return true; + if (simpleClassNeighbor.contains(simpleClassTarget) || simpleClassTarget.contains(simpleClassNeighbor)) return true; + + List impls = context.getImplementations(classTarget); + if (impls != null) { + for (String impl : impls) { + if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true; + } + } + List implsN = context.getImplementations(classNeighbor); + if (implsN != null) { + for (String impl : implsN) { + if (impl.equals(classTarget) || impl.endsWith("." + classTarget)) return true; + } + } + + return false; + } + + String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target; + String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor; + return simpleNeighbor.equals(simpleTarget); + } + + private boolean isFullyQualifiedMethod(String methodFqn) { + if (methodFqn == null || !methodFqn.contains(".")) return false; + int lastDot = methodFqn.lastIndexOf('.'); + String classPart = methodFqn.substring(0, lastDot); + if (classPart.contains(".")) return true; + if (!classPart.isEmpty() && Character.isUpperCase(classPart.charAt(0))) return true; + return false; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java new file mode 100644 index 0000000..5617e46 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java @@ -0,0 +1,374 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.eclipse.jdt.core.dom.*; +import java.util.*; +import java.util.function.Function; + +public class ConstantExtractor { + private final CodebaseContext context; + private final ConstantResolver constantResolver; + private final Function calledMethodResolver; + private VariableTracer variableTracer; + private ConstructorAnalyzer constructorAnalyzer; + + public ConstantExtractor(CodebaseContext context, ConstantResolver constantResolver, Function calledMethodResolver) { + this.context = context; + this.constantResolver = constantResolver; + this.calledMethodResolver = calledMethodResolver; + } + + public void setVariableTracer(VariableTracer variableTracer) { + this.variableTracer = variableTracer; + } + + public void setConstructorAnalyzer(ConstructorAnalyzer constructorAnalyzer) { + this.constructorAnalyzer = constructorAnalyzer; + } + + public void extractConstantsFromExpression(Expression expr, List constants) { + if (expr instanceof QualifiedName qn) { + constants.add(qn.toString()); + } else if (expr instanceof StringLiteral sl) { + constants.add(sl.getLiteralValue()); + } else if (expr instanceof ConditionalExpression ce) { + extractConstantsFromExpression(ce.getThenExpression(), constants); + extractConstantsFromExpression(ce.getElseExpression(), constants); + } else if (expr instanceof ParenthesizedExpression pe) { + extractConstantsFromExpression(pe.getExpression(), constants); + } else if (expr instanceof CastExpression ce) { + extractConstantsFromExpression(ce.getExpression(), constants); + } else if (expr instanceof Assignment assignment) { + extractConstantsFromExpression(assignment.getRightHandSide(), constants); + } else if (expr instanceof SwitchExpression se) { + se.accept(new ASTVisitor() { + @Override + public boolean visit(YieldStatement ys) { + extractConstantsFromExpression(ys.getExpression(), constants); + return super.visit(ys); + } + @Override + public boolean visit(ExpressionStatement es) { + extractConstantsFromExpression(es.getExpression(), constants); + return super.visit(es); + } + @Override + public boolean visit(ReturnStatement rs) { + extractConstantsFromExpression(rs.getExpression(), constants); + return super.visit(rs); + } + }); + } else if (expr instanceof ArrayAccess aa) { + extractConstantsFromExpression(aa.getArray(), constants); + } else if (expr instanceof ArrayCreation ac && ac.getInitializer() != null) { + for (Object expObj : ac.getInitializer().expressions()) { + extractConstantsFromArgument((Expression) expObj, constants); + } + } else if (expr instanceof ArrayInitializer ai) { + for (Object expObj : ai.expressions()) { + extractConstantsFromArgument((Expression) expObj, constants); + } + } else if (expr instanceof MethodInvocation mi) { + String methodName = mi.getName().getIdentifier(); + + if (methodName.equals("get") && mi.arguments().size() == 1 && mi.getExpression() != null) { + extractConstantsFromExpression(mi.getExpression(), constants); + return; + } + if ((methodName.equals("of") || methodName.equals("asList")) && mi.getExpression() instanceof SimpleName) { + for (Object argObj : mi.arguments()) { + extractConstantsFromExpression((Expression) argObj, constants); + } + return; + } + + if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof SimpleName sn) { + String varName = sn.getIdentifier(); + String propName = methodName.startsWith("get") ? methodName.substring(3) : methodName; + + Block block = findEnclosingBlock(mi); + if (block != null) { + for (Object stmtObj : block.statements()) { + if (stmtObj == mi.getParent() || stmtObj == mi) break; + if (stmtObj instanceof ExpressionStatement es) { + if (es.getExpression() instanceof MethodInvocation setterMi) { + if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) { + if (setterMi.getExpression() instanceof SimpleName setterSn && setterSn.getIdentifier().equals(varName)) { + if (!setterMi.arguments().isEmpty()) { + extractConstantsFromExpression((Expression) setterMi.arguments().get(0), constants); + return; + } + } + } + } else if (es.getExpression() instanceof Assignment assignment) { + if (assignment.getLeftHandSide() instanceof FieldAccess fa) { + if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) { + if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) { + extractConstantsFromExpression(assignment.getRightHandSide(), constants); + return; + } + } + } else if (assignment.getLeftHandSide() instanceof QualifiedName qqn) { + if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) { + if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) { + extractConstantsFromExpression(assignment.getRightHandSide(), constants); + return; + } + } + } + } + } + } + } + } + + if (mi.getExpression() instanceof ClassInstanceCreation cic) { + if (cic.getAnonymousClassDeclaration() != null) { + for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) { + if (declObj instanceof MethodDeclaration mdecl) { + if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) { + mdecl.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement rs) { + if (rs.getExpression() != null) { + extractConstantsFromExpression(rs.getExpression(), constants); + } + return super.visit(rs); + } + }); + } + } + } + } else if (constructorAnalyzer != null) { + boolean handledByLombok = false; + if (methodName.startsWith("get") && methodName.length() > 3) { + String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + TypeDeclaration typeDecl = context.getTypeDeclaration(typeName); + if (typeDecl != null) { + String fieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); + int fieldIndex = constructorAnalyzer.findConstructorArgumentIndexForLombokField(typeDecl, fieldName); + if (fieldIndex >= 0 && fieldIndex < cic.arguments().size()) { + extractConstantsFromArgument((Expression) cic.arguments().get(fieldIndex), constants); + handledByLombok = true; + } + } + } + if (!handledByLombok && methodName != null && !methodName.isEmpty()) { + List narrowed = constructorAnalyzer.resolveInlineInstantiationGetterResult( + cic, methodName, context, new HashSet<>()); + if (narrowed.isEmpty()) { + String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + CompilationUnit contextCu = expr.getRoot() instanceof CompilationUnit ? (CompilationUnit) expr.getRoot() : null; + TypeDeclaration typeDecl = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName); + if (typeDecl == null) typeDecl = context.getTypeDeclaration(typeName); + if (typeDecl != null) { + narrowed = resolveMethodReturnConstant(context.getFqn(typeDecl), methodName, 0, new HashSet<>(), contextCu); + } + } + if (narrowed != null && !narrowed.isEmpty()) { + constants.addAll(narrowed); + handledByLombok = true; + } + } + if (!handledByLombok) { + for (Object argObj : cic.arguments()) { + extractConstantsFromArgument((Expression) argObj, constants); + } + } + } + } + + Expression currentMi = mi; + while (currentMi instanceof MethodInvocation chainMi) { + String chainName = chainMi.getName().getIdentifier(); + if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) { + extractConstantsFromExpression((Expression) chainMi.arguments().get(1), constants); + } else if ((chainName.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) { + extractConstantsFromExpression((Expression) chainMi.arguments().get(0), constants); + } + currentMi = chainMi.getExpression(); + } + + TypeDeclaration td = findEnclosingType(mi); + if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof ThisExpression)) { + List values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new HashSet<>(), null); + if (values != null) constants.addAll(values); + } + } + } + + public void extractConstantsFromArgument(Expression argObj, List constants) { + if (argObj instanceof ClassInstanceCreation innerCic) { + String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType()); + if ("String".equals(typeName)) { + extractConstantsFromExpression(argObj, constants); + } + } else { + extractConstantsFromExpression(argObj, constants); + } + } + + public List resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) { + if (className != null && className.contains(".")) { + String prefix = className.substring(0, className.lastIndexOf('.')); + String suffix = className.substring(className.lastIndexOf('.') + 1); + TypeDeclaration prefixTd = contextCu != null ? context.getTypeDeclaration(prefix, contextCu) : context.getTypeDeclaration(prefix); + if (prefixTd == null) prefixTd = context.getTypeDeclaration(prefix); + boolean isKnownType = prefixTd != null || context.getEnumValues(prefix) != null; + if (isKnownType) { + String simplePrefix = prefix.contains(".") ? prefix.substring(prefix.lastIndexOf('.') + 1) : prefix; + return Collections.singletonList(simplePrefix + "." + suffix); + } + } + + TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className); + if (td == null) td = context.getTypeDeclaration(className); + if (td == null) return null; + + final List resolvedConstants = new ArrayList<>(); + for (MethodDeclaration md : td.getMethods()) { + if (md.getBody() != null) { + md.getBody().accept(new ASTVisitor() { + public boolean visit(ReturnStatement node) { + if (variableTracer != null) { + List tracedReturns = variableTracer.traceVariableAll(node.getExpression()); + for (Expression tracedRet : tracedReturns) { + extractConstantsFromExpression(tracedRet, resolvedConstants); + } + } + return super.visit(node); + } + }); + } + if (!resolvedConstants.isEmpty()) { + return resolvedConstants; + } + } + return null; + } + + public List resolveMethodReturnConstant(String className, String methodName, int depth, Set visited, CompilationUnit contextCu) { + if (depth > 20) return Collections.emptyList(); + String fqn = className + "." + methodName; + if (!visited.add(fqn)) return Collections.emptyList(); + + List constants = new ArrayList<>(); + TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null; + if (tempTd == null) { + tempTd = context.getTypeDeclaration(className); + } + final TypeDeclaration td = tempTd; + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && md.getBody() != null) { + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + Expression retExpr = node.getExpression(); + if (retExpr != null) { + boolean handled = false; + if (retExpr instanceof MethodInvocation mi && calledMethodResolver != null) { + String called = calledMethodResolver.apply(mi); + if (called != null && called.contains(".")) { + if (visited.contains(called)) { + handled = true; + } else { + String cName = called.substring(0, called.lastIndexOf('.')); + String mName = called.substring(called.lastIndexOf('.') + 1); + + TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); + if (targetTd == null) targetTd = context.getTypeDeclaration(cName); + + if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { + List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); + constants.addAll(delegationResult); + handled = true; + } + } + } + } else if (retExpr instanceof SuperMethodInvocation smi) { + String superFqn = context.getSuperclassFqn(td); + if (superFqn != null) { + String called = superFqn + "." + smi.getName().getIdentifier(); + if (visited.contains(called)) { + handled = true; + } else { + String cName = superFqn; + String mName = smi.getName().getIdentifier(); + TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); + if (targetTd == null) targetTd = context.getTypeDeclaration(cName); + + if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { + List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); + constants.addAll(delegationResult); + handled = true; + } + } + } + } + if (!handled && variableTracer != null && constructorAnalyzer != null) { + List tracedRetExprs = variableTracer.traceVariableAll(retExpr); + for (Expression tracedRetExpr : tracedRetExprs) { + extractConstantsFromExpression(tracedRetExpr, constants); + String val = constantResolver.resolve(tracedRetExpr, context); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + for (String eVal : val.substring(9).split(",")) { + String parsed = parseEnumSetElement(eVal); + if (!constants.contains(parsed)) constants.add(parsed); + } + } else { + if (!constants.contains(val)) constants.add(val); + } + } else if (tracedRetExpr instanceof SimpleName sn) { + List consts = constructorAnalyzer.traceFieldInConstructors(td, sn.getIdentifier(), context, visited); + if (!consts.isEmpty()) { + constants.addAll(consts); + } + } else if (tracedRetExpr instanceof FieldAccess fa) { + List consts = constructorAnalyzer.traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited); + if (!consts.isEmpty()) { + constants.addAll(consts); + } + } + } + } + } + return super.visit(node); + } + }); + } else { + List impls = context.getImplementations(className); + if (impls != null) { + for (String implName : impls) { + List delegationResult = resolveMethodReturnConstant(implName, methodName, depth + 1, visited, contextCu); + constants.addAll(delegationResult); + } + } + } + } + visited.remove(fqn); + return constants; + } + + public String parseEnumSetElement(String eVal) { + return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal; + } + + private Block findEnclosingBlock(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof Block)) { + parent = parent.getParent(); + } + return (Block) parent; + } + + private TypeDeclaration findEnclosingType(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof TypeDeclaration)) { + parent = parent.getParent(); + } + return (TypeDeclaration) parent; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java new file mode 100644 index 0000000..aa76c0d --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorAnalyzer.java @@ -0,0 +1,649 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.eclipse.jdt.core.dom.*; +import java.util.*; + +public class ConstructorAnalyzer { + private final CodebaseContext context; + private final ConstantResolver constantResolver; + private VariableTracer variableTracer; + private ConstantExtractor constantExtractor; + + @FunctionalInterface + public interface RhsResolver { + String resolve(Expression rhs, Map paramValues, TypeDeclaration td); + } + + public ConstructorAnalyzer(CodebaseContext context, ConstantResolver constantResolver) { + this.context = context; + this.constantResolver = constantResolver; + } + + public void setVariableTracer(VariableTracer variableTracer) { + this.variableTracer = variableTracer; + } + + public void setConstantExtractor(ConstantExtractor constantExtractor) { + this.constantExtractor = constantExtractor; + } + + public List traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set visited) { + List results = new ArrayList<>(); + + for (FieldDeclaration fd : td.getFields()) { + for (Object fragObj : fd.fragments()) { + VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) { + Expression right = frag.getInitializer(); + if (variableTracer != null && constantExtractor != null) { + List tracedRights = variableTracer.traceVariableAll(right); + for (Expression tracedRight : tracedRights) { + if (tracedRight instanceof MethodInvocation mi) { + String calledName = mi.getName().getIdentifier(); + String targetClass = context.getFqn(td); + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + if (mi.getExpression() == null) { + TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu); + if (targetTd != null) { + targetClass = context.getFqn(targetTd); + } + } + results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu)); + } else { + String val = constantResolver.resolve(tracedRight, context); + if (val != null) results.add(val); + } + } + } + } + } + } + + ASTVisitor assignmentVisitor = new ASTVisitor() { + @Override + public boolean visit(Assignment node) { + Expression left = node.getLeftHandSide(); + if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) || + (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) { + + Expression right = node.getRightHandSide(); + if (variableTracer != null && constantExtractor != null) { + List tracedRights = variableTracer.traceVariableAll(right); + for (Expression tracedRight : tracedRights) { + if (tracedRight instanceof MethodInvocation mi) { + String calledName = mi.getName().getIdentifier(); + String targetClass = context.getFqn(td); + + if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) { + targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName(); + } else if (mi.getExpression() instanceof SimpleName) { + String exprName = ((SimpleName) mi.getExpression()).getIdentifier(); + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu); + if (targetTd != null) { + targetClass = context.getFqn(targetTd); + } + } else if (mi.getExpression() == null) { + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu); + if (targetTd != null) { + targetClass = context.getFqn(targetTd); + } + } + + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + results.addAll(constantExtractor.resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu)); + } else { + String val = constantResolver.resolve(tracedRight, context); + if (val != null) results.add(val); + } + } + } + } + return super.visit(node); + } + @Override + public boolean visit(ConstructorInvocation node) { + processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited); + return super.visit(node); + } + + @Override + public boolean visit(SuperConstructorInvocation node) { + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + String superFqn = context.getSuperclassFqn(enclosingTd); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited); + } + } + return super.visit(node); + } + }; + + for (MethodDeclaration md : td.getMethods()) { + if (md.isConstructor() && md.getBody() != null) { + md.getBody().accept(assignmentVisitor); + } + } + + for (Object bodyDecl : td.bodyDeclarations()) { + if (bodyDecl instanceof Initializer init && init.getBody() != null) { + init.getBody().accept(assignmentVisitor); + } + } + + return results; + } + + public Map buildFieldValuesFromCIC( + TypeDeclaration td, + ClassInstanceCreation cic, + CodebaseContext context, + RhsResolver rhsResolver) { + + String actualTypeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + TypeDeclaration actualTd = context.getTypeDeclaration(actualTypeName); + if (actualTd == null) { + actualTd = td; + } + + Map fieldValues = new HashMap<>(); + Set visitedCtors = new HashSet<>(); + buildFieldValuesFromCICRecursive(actualTd, cic, context, fieldValues, visitedCtors, rhsResolver); + return fieldValues; + } + + public Map buildFieldValuesFromCIC( + TypeDeclaration td, + ClassInstanceCreation cic, + CodebaseContext context) { + return buildFieldValuesFromCIC(td, cic, context, null); + } + + private void buildFieldValuesFromCICRecursive( + TypeDeclaration td, + ClassInstanceCreation cic, + CodebaseContext context, + Map fieldValues, + Set visitedCtors, + RhsResolver rhsResolver) { + + String tdKey = context.getFqn(td); + if (!visitedCtors.add(tdKey)) return; + + for (MethodDeclaration ctor : td.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<>(); + if (constantExtractor != null) { + constantExtractor.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 paramVal = null; + if (rhs instanceof 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); + } + } + return super.visit(node); + } + }); + + ctor.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(SuperConstructorInvocation sci) { + String superFqn = context.getSuperclassFqn(td); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + if (superTd != null) { + processSuperConstructorArgs(superTd, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver); + } + } + return super.visit(sci); + } + }); + break; + } + } + + private void processSuperConstructorArgs( + TypeDeclaration superTd, + List superArgs, + Map subClassParamValues, + CodebaseContext context, + Map fieldValues, + Set visitedCtors, + RhsResolver rhsResolver) { + + if (superTd == null) return; + + for (MethodDeclaration superCtor : superTd.getMethods()) { + if (!superCtor.isConstructor() || superCtor.getBody() == null) continue; + if (superCtor.parameters().size() != superArgs.size()) continue; + + Map superParamValues = new HashMap<>(); + for (int i = 0; i < superCtor.parameters().size(); i++) { + String pName = ((SingleVariableDeclaration) superCtor.parameters().get(i)).getName().getIdentifier(); + Expression argExpr = (Expression) superArgs.get(i); + + String resolved = null; + if (argExpr instanceof SimpleName sn) { + resolved = subClassParamValues.get(sn.getIdentifier()); + } + if (resolved == null) { + List consts = new ArrayList<>(); + if (constantExtractor != null) { + constantExtractor.extractConstantsFromArgument(argExpr, consts); + } + if (!consts.isEmpty()) { + resolved = consts.get(0); + } else { + resolved = constantResolver.resolve(argExpr, context); + } + } + if (resolved != null) { + superParamValues.put(pName, resolved); + } + } + if (superParamValues.isEmpty()) continue; + + superCtor.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 + && !superParamValues.containsKey(sn.getIdentifier())) { + fieldName = sn.getIdentifier(); + } + + if (fieldName != null) { + String paramVal = null; + if (rhs instanceof 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); + } + } + return super.visit(node); + } + }); + + superCtor.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(SuperConstructorInvocation sci) { + String superFqn = context.getSuperclassFqn(superTd); + if (superFqn != null) { + TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn); + if (nextSuperTd != null) { + processSuperConstructorArgs(nextSuperTd, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver); + } + } + return super.visit(sci); + } + }); + break; + } + } + + public String evaluateMethodCallInConstructor( + Expression rhs, + Map paramValues, + TypeDeclaration td) { + + if (!(rhs instanceof MethodInvocation mi)) { + return null; + } + + String methodName = mi.getName().getIdentifier(); + Expression receiver = mi.getExpression(); + if (receiver != null && !(receiver instanceof ThisExpression)) { + return null; + } + + MethodDeclaration method = context.findMethodDeclaration(td, methodName, true); + if (method == null || method.getBody() == null) { + return null; + } + + Map localVars = new HashMap<>(paramValues); + return constantResolver.evaluateMethodBodyWithLocals(method, localVars, context, new HashSet<>()); + } + + public ClassInstanceCreation findReturnedCIC( + String className, String methodName, CodebaseContext context) { + return findReturnedCIC(className, methodName, context, new HashSet<>(), 0); + } + + private ClassInstanceCreation findReturnedCIC( + String className, String methodName, CodebaseContext context, + Set visited, int depth) { + if (depth > 50 || !visited.add(className + "#" + methodName)) { + return null; + } + + TypeDeclaration td = context.getTypeDeclaration(className); + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && md.getBody() != null) { + ClassInstanceCreation[] result = {null}; + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement rs) { + if (rs.getExpression() instanceof ClassInstanceCreation cic) { + result[0] = cic; + } + return false; + } + }); + if (result[0] != null) return result[0]; + } + } + List impls = context.getImplementations(className); + for (String impl : impls) { + ClassInstanceCreation cic = findReturnedCIC(impl, methodName, context, visited, depth + 1); + if (cic != null) return cic; + } + return null; + } + + public List resolveInlineInstantiationGetterResult( + ClassInstanceCreation cic, + String getterName, + CodebaseContext context, + Set visited) { + + String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + CompilationUnit contextCu = cic.getRoot() instanceof CompilationUnit ? (CompilationUnit) cic.getRoot() : null; + TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName); + if (td == null) td = context.getTypeDeclaration(typeName); + if (td == null) return Collections.emptyList(); + + MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true); + if (getter == null || getter.getBody() == null) return Collections.emptyList(); + + String getterKey = typeName + "#" + getterName; + if (!visited.add(getterKey)) { + return Collections.emptyList(); + } + + try { + Map fieldValues = buildFieldValuesFromCIC(td, cic, context); + if (fieldValues.isEmpty()) return Collections.emptyList(); + + String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited); + return result != null ? List.of(result) : Collections.emptyList(); + } finally { + visited.remove(getterKey); + } + } + + public int findConstructorArgumentIndexForLombokField(TypeDeclaration td, String fieldName) { + boolean isAllArgsConstructor = false; + boolean isRequiredArgsConstructor = false; + boolean isData = false; + boolean isValue = false; + + for (Object modObj : td.modifiers()) { + if (modObj instanceof MarkerAnnotation ma) { + String name = ma.getTypeName().getFullyQualifiedName(); + if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true; + if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true; + if (name.equals("Data") || name.equals("lombok.Data")) isData = true; + if (name.equals("Value") || name.equals("lombok.Value")) isValue = true; + } else if (modObj instanceof NormalAnnotation na) { + String name = na.getTypeName().getFullyQualifiedName(); + if (name.equals("AllArgsConstructor") || name.equals("lombok.AllArgsConstructor")) isAllArgsConstructor = true; + if (name.equals("RequiredArgsConstructor") || name.equals("lombok.RequiredArgsConstructor")) isRequiredArgsConstructor = true; + if (name.equals("Data") || name.equals("lombok.Data")) isData = true; + if (name.equals("Value") || name.equals("lombok.Value")) isValue = true; + } + } + + if (!isAllArgsConstructor && !isRequiredArgsConstructor && !isData && !isValue) { + return -1; + } + + int index = 0; + for (FieldDeclaration fd : td.getFields()) { + boolean isStatic = false; + boolean isFinal = false; + boolean hasNonNull = false; + for (Object modObj : fd.modifiers()) { + if (modObj instanceof Modifier mod) { + if (mod.isStatic()) isStatic = true; + if (mod.isFinal()) isFinal = true; + } else if (modObj instanceof MarkerAnnotation ma) { + if (ma.getTypeName().getFullyQualifiedName().endsWith("NonNull")) hasNonNull = true; + } + } + if (isStatic) continue; + + if (isRequiredArgsConstructor && !isAllArgsConstructor && !isFinal && !hasNonNull) continue; + if (isData && !isAllArgsConstructor && !isFinal && !hasNonNull) continue; + + for (Object fragObj : fd.fragments()) { + VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(fieldName)) { + return index; + } + index++; + } + } + return -1; + } + + private void processConstructorInvocationArgs(List arguments, TypeDeclaration targetTd, ASTNode callNode, List results, String fieldName, CodebaseContext context, Set visited) { + if (targetTd == null) return; + MethodDeclaration callerMd = findEnclosingMethod(callNode); + + IMethodBinding resolvedBinding = null; + if (callNode instanceof ConstructorInvocation ci) { + resolvedBinding = ci.resolveConstructorBinding(); + } else if (callNode instanceof SuperConstructorInvocation sci) { + resolvedBinding = sci.resolveConstructorBinding(); + } + + for (MethodDeclaration otherMd : targetTd.getMethods()) { + boolean matches = false; + if (resolvedBinding != null && otherMd.resolveBinding() != null) { + matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey()); + } else { + matches = otherMd.isConstructor() && otherMd.parameters().size() == arguments.size() && otherMd != callerMd; + } + + if (matches) { + int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new HashSet<>()); + if (targetIdx >= 0 && targetIdx < arguments.size()) { + Expression arg = (Expression) arguments.get(targetIdx); + String val = constantResolver.resolve(arg, context); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + for (String eVal : val.substring(9).split(",")) { + if (constantExtractor != null) { + results.add(constantExtractor.parseEnumSetElement(eVal)); + } + } + } else { + results.add(val); + } + } + } + } + } + } + + private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, Set visited) { + if (constructorMd == null || constructorMd.getBody() == null) return -1; + if (!visited.add(constructorMd)) return -1; + + final int[] foundIdx = {-1}; + constructorMd.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(Assignment asn) { + Expression left = asn.getLeftHandSide(); + if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) || + (left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) || + (left instanceof SuperFieldAccess sfa && sfa.getName().getIdentifier().equals(fieldName))) { + Expression right = asn.getRightHandSide(); + String rightName = extractVariableName(right); + if (rightName != null) { + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(rightName)) { + foundIdx[0] = i; + } + } + } + } + return super.visit(asn); + } + @Override + public boolean visit(ConstructorInvocation node) { + IMethodBinding resolvedBinding = node.resolveConstructorBinding(); + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + for (MethodDeclaration otherMd : enclosingTd.getMethods()) { + boolean matches = false; + if (resolvedBinding != null && otherMd.resolveBinding() != null) { + matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey()); + } else { + matches = otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size(); + } + if (matches) { + int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited); + if (targetIdx >= 0 && targetIdx < node.arguments().size()) { + Expression arg = (Expression) node.arguments().get(targetIdx); + String argName = extractVariableName(arg); + if (argName != null) { + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(argName)) { + foundIdx[0] = i; + } + } + } + } + } + } + } + return super.visit(node); + } + @Override + public boolean visit(SuperConstructorInvocation node) { + IMethodBinding resolvedBinding = node.resolveConstructorBinding(); + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + String superFqn = context.getSuperclassFqn(enclosingTd); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + if (superTd != null) { + for (MethodDeclaration otherMd : superTd.getMethods()) { + boolean matches = false; + if (resolvedBinding != null && otherMd.resolveBinding() != null) { + matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey()); + } else { + matches = otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size(); + } + if (matches) { + int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited); + if (targetIdx >= 0 && targetIdx < node.arguments().size()) { + Expression arg = (Expression) node.arguments().get(targetIdx); + String argName = extractVariableName(arg); + if (argName != null) { + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(argName)) { + foundIdx[0] = i; + } + } + } + } + } + } + } + } + } + return super.visit(node); + } + }); + return foundIdx[0]; + } + + private String extractVariableName(Expression expr) { + if (expr instanceof SimpleName sn) return sn.getIdentifier(); + if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier(); + if (expr instanceof SuperFieldAccess sfa) return sfa.getName().getIdentifier(); + if (expr instanceof CastExpression ce) return extractVariableName(ce.getExpression()); + if (expr instanceof ParenthesizedExpression pe) return extractVariableName(pe.getExpression()); + if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) { + if (!mi.arguments().isEmpty()) return extractVariableName((Expression)mi.arguments().get(0)); + } + return null; + } + + private MethodDeclaration findEnclosingMethod(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof MethodDeclaration)) { + parent = parent.getParent(); + } + return (MethodDeclaration) parent; + } + + private TypeDeclaration findEnclosingType(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof TypeDeclaration)) { + parent = parent.getParent(); + } + return (TypeDeclaration) parent; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java new file mode 100644 index 0000000..efa2d7b --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java @@ -0,0 +1,133 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.eclipse.jdt.core.dom.*; +import java.util.*; + +public class TypeResolver { + private final CodebaseContext context; + + public TypeResolver(CodebaseContext context) { + this.context = context; + } + + public int getParameterIndex(String methodFqn, String paramName) { + if (methodFqn == null || !methodFqn.contains(".")) return -1; + String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); + String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); + TypeDeclaration td = context.getTypeDeclaration(className); + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null) { + for (int i = 0; i < md.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i); + if (svd.getName().getIdentifier().equals(paramName)) { + return i; + } + } + } + } + return -1; + } + + public String getParameterType(String methodFqn, int index) { + if (methodFqn == null || !methodFqn.contains(".") || index < 0) return null; + String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); + String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); + TypeDeclaration td = context.getTypeDeclaration(className); + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && index < md.parameters().size()) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(index); + return svd.getType().toString(); + } + } + return null; + } + + public boolean isTypeCompatible(String actualType, String expectedType) { + if (actualType == null || expectedType == null) return true; + if (expectedType.equals("Object") || expectedType.equals("java.lang.Object")) return true; + if (actualType.equals("Object") || actualType.equals("java.lang.Object")) return true; + if (expectedType.length() == 1 && Character.isUpperCase(expectedType.charAt(0))) return true; + if (actualType.length() == 1 && Character.isUpperCase(actualType.charAt(0))) return true; + + String originalExpectedType = expectedType; + + if (actualType.contains("<")) { + String rawType = actualType.substring(0, actualType.indexOf('<')); + if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) { + int start = actualType.indexOf('<'); + int end = actualType.lastIndexOf('>'); + if (start >= 0 && end > start) { + actualType = actualType.substring(start + 1, end); + } + } + } + if (expectedType.contains("<")) { + String rawType = expectedType.substring(0, expectedType.indexOf('<')); + if (rawType.equals("List") || rawType.equals("Set") || rawType.equals("Collection") || rawType.equals("Iterable") || rawType.endsWith("List") || rawType.endsWith("Set") || rawType.endsWith("Collection")) { + int start = expectedType.indexOf('<'); + int end = expectedType.lastIndexOf('>'); + if (start >= 0 && end > start) { + expectedType = expectedType.substring(start + 1, end); + } + } + } + + if (actualType.contains("<")) { + actualType = actualType.substring(0, actualType.indexOf('<')); + } + if (expectedType.contains("<")) { + expectedType = expectedType.substring(0, expectedType.indexOf('<')); + } + + if (actualType.equals(expectedType)) return true; + if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true; + + AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType); + if (td != null) { + if (td instanceof TypeDeclaration typeDecl) { + String superFqn = context.getSuperclassFqn(typeDecl); + while (superFqn != null) { + if (superFqn.contains("<")) { + superFqn = superFqn.substring(0, superFqn.indexOf('<')); + } + if (superFqn.equals(expectedType) || superFqn.endsWith("." + expectedType) || expectedType.endsWith("." + superFqn)) return true; + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + superFqn = superTd != null ? context.getSuperclassFqn(superTd) : null; + } + } + List interfaces = java.util.Collections.emptyList(); + if (td instanceof TypeDeclaration typeDecl) { + interfaces = typeDecl.superInterfaceTypes(); + } else if (td instanceof EnumDeclaration enumDecl) { + interfaces = enumDecl.superInterfaceTypes(); + } else if (td instanceof RecordDeclaration recordDecl) { + interfaces = recordDecl.superInterfaceTypes(); + } + for (Object interfaceType : interfaces) { + String itStr = interfaceType.toString(); + if (itStr.contains("<")) { + itStr = itStr.substring(0, itStr.indexOf('<')); + } + if (itStr.equals(expectedType) || itStr.endsWith("." + expectedType) || expectedType.endsWith("." + itStr)) return true; + } + } + + if (originalExpectedType.contains("<")) { + int start = originalExpectedType.indexOf('<'); + int end = originalExpectedType.lastIndexOf('>'); + if (start >= 0 && end > start) { + String sub = originalExpectedType.substring(start + 1, end); + for (String part : sub.split(",")) { + String typeArg = part.trim(); + if (isTypeCompatible(actualType, typeArg)) { + return true; + } + } + } + } + return false; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java new file mode 100644 index 0000000..6ee3fe2 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/VariableTracer.java @@ -0,0 +1,643 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallEdge; +import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.eclipse.jdt.core.dom.*; +import java.util.*; + +public class VariableTracer { + private final CodebaseContext context; + private final ConstantResolver constantResolver; + private ConstantExtractor constantExtractor; + + public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) { + this.context = context; + this.constantResolver = constantResolver; + } + + public void setConstantExtractor(ConstantExtractor constantExtractor) { + this.constantExtractor = constantExtractor; + } + + public Expression traceVariable(Expression expr) { + List all = traceVariableAll(expr, new HashSet<>()); + return all.isEmpty() ? expr : all.get(all.size() - 1); + } + + public List traceVariableAll(Expression expr) { + return traceVariableAll(expr, new HashSet<>()); + } + + public List traceVariableAll(Expression expr, Set visitedVariables) { + List results = new ArrayList<>(); + if (expr instanceof SimpleName sn) { + String varName = sn.getIdentifier(); + String methodKey = ""; + MethodDeclaration enclosingMethod = findEnclosingMethod(expr); + if (enclosingMethod != null) { + TypeDeclaration enclosingType = findEnclosingType(enclosingMethod); + if (enclosingType != null) { + methodKey = context.getFqn(enclosingType) + "." + enclosingMethod.getName().getIdentifier() + "#" + varName; + } + } + if (methodKey.isEmpty()) { + methodKey = varName; + } + if (!visitedVariables.add(methodKey)) { + return results; // Break infinite loop + } + if (enclosingMethod != null) { + enclosingMethod.accept(new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationFragment node) { + if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { + results.addAll(traceVariableAll(node.getInitializer(), visitedVariables)); + } + return super.visit(node); + } + @Override + public boolean visit(Assignment node) { + if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { + results.addAll(traceVariableAll(node.getRightHandSide(), visitedVariables)); + } + return super.visit(node); + } + }); + if (!results.isEmpty()) { + visitedVariables.remove(methodKey); + return results; + } + + // Tracing parameter callers (Inter-procedural within the same class) + for (Object paramObj : enclosingMethod.parameters()) { + SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj; + if (param.getName().getIdentifier().equals(varName)) { + int paramIndex = enclosingMethod.parameters().indexOf(param); + TypeDeclaration enclosingType = findEnclosingType(enclosingMethod); + if (enclosingType != null) { + String mName = enclosingMethod.getName().getIdentifier(); + List callerExprs = new ArrayList<>(); + enclosingType.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + if (node.getName().getIdentifier().equals(mName) && node.arguments().size() > paramIndex) { + callerExprs.addAll(traceVariableAll((Expression) node.arguments().get(paramIndex), visitedVariables)); + } + return super.visit(node); + } + @Override + public boolean visit(ExpressionMethodReference node) { + if (node.getName().getIdentifier().equals(mName)) { + if (node.getParent() instanceof MethodInvocation parentMi) { + if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(parentMi)) { + callerExprs.addAll(traceVariableAll(parentMi.getExpression(), visitedVariables)); + } else { + for (Object arg : parentMi.arguments()) { + if (arg != node) { + callerExprs.addAll(traceVariableAll((Expression) arg, visitedVariables)); + } + } + } + } + } + return super.visit(node); + } + }); + if (!callerExprs.isEmpty()) { + visitedVariables.remove(methodKey); + return callerExprs; + } + } + } + } + } + visitedVariables.remove(methodKey); + } + results.add(expr); + return results; + } + + public Expression traceLocalSetter(String methodFqn, String varName, String getterName) { + 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) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && md.getBody() != null) { + final Expression[] setterArg = new Expression[1]; + String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName; + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + if (node.getName().getIdentifier().equalsIgnoreCase("set" + propName) || node.getName().getIdentifier().equalsIgnoreCase(propName)) { + if (node.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) { + if (!node.arguments().isEmpty()) { + setterArg[0] = (Expression) node.arguments().get(0); + } + } + } + return super.visit(node); + } + @Override + public boolean visit(Assignment node) { + if (node.getLeftHandSide() instanceof FieldAccess fa) { + if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) { + if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) { + setterArg[0] = node.getRightHandSide(); + } + } + } else if (node.getLeftHandSide() instanceof QualifiedName qqn) { + if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) { + if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) { + setterArg[0] = node.getRightHandSide(); + } + } + } + return super.visit(node); + } + }); + return setterArg[0]; + } + } + return null; + } + + public String getVariableDeclaredType(String methodFqn, String cleanFieldName) { + if (methodFqn == null) return null; + int lastDot = methodFqn.lastIndexOf('.'); + if (lastDot > 0) { + String fqn = methodFqn.substring(0, lastDot); + String methodName = methodFqn.substring(lastDot + 1); + TypeDeclaration td = context.getTypeDeclaration(fqn); + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, false); + if (md != null) { + for (Object pObj : md.parameters()) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj; + if (svd.getName().getIdentifier().equals(cleanFieldName)) { + return svd.getType().toString(); + } + } + final String[] foundType = new String[1]; + if (md.getBody() != null) { + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationStatement node) { + for (Object fragObj : node.fragments()) { + VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(cleanFieldName)) { + foundType[0] = node.getType().toString(); + } + } + return super.visit(node); + } + + @Override + public boolean visit(LambdaExpression node) { + for (Object paramObj : node.parameters()) { + VariableDeclaration param = (VariableDeclaration) paramObj; + if (param.getName().getIdentifier().equals(cleanFieldName)) { + if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) { + foundType[0] = svd.getType().toString(); + return false; + } + + ASTNode parent = node.getParent(); + if (parent instanceof MethodInvocation mi) { + Expression caller = mi.getExpression(); + + boolean hasMap = false; + while (caller instanceof MethodInvocation chainMi) { + String mName = chainMi.getName().getIdentifier(); + if (mName.equals("map") || mName.equals("flatMap")) { + hasMap = true; + if (chainMi.arguments().size() == 1 && chainMi.arguments().get(0) instanceof LambdaExpression lambda) { + if (lambda.getBody() instanceof MethodInvocation mapMi) { + Expression mapCaller = mapMi.getExpression(); + if (mapCaller instanceof SimpleName mapSn) { + String mapCallerType = getVariableDeclaredType(methodFqn, mapSn.getIdentifier()); + if (mapCallerType != null) { + TypeDeclaration currentTd = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))); + CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null; + TypeDeclaration td = context.getTypeDeclaration(mapCallerType, cu); + if (td != null) { + MethodDeclaration mapMd = context.findMethodDeclaration(td, mapMi.getName().getIdentifier(), true); + if (mapMd != null && mapMd.getReturnType2() != null) { + foundType[0] = mapMd.getReturnType2().toString(); + return false; + } + } + } + } + } + } + } + if (mName.equals("stream") || mName.equals("filter") || mName.equals("map") || + mName.equals("flatMap") || mName.equals("peek") || mName.equals("distinct") || + mName.equals("sorted") || mName.equals("limit") || mName.equals("skip")) { + caller = chainMi.getExpression(); + } else { + break; + } + } + + if (!hasMap) { + if (caller instanceof SimpleName callerSn) { + String callerName = callerSn.getIdentifier(); + if (!callerName.equals(cleanFieldName)) { + String callerType = getVariableDeclaredType(methodFqn, callerName); + if (callerType != null && callerType.contains("<")) { + int start = callerType.indexOf('<'); + int end = callerType.lastIndexOf('>'); + if (start >= 0 && end > start) { + foundType[0] = callerType.substring(start + 1, end); + return false; + } + } + } + } else if (caller instanceof FieldAccess fa) { + String callerName = fa.getName().getIdentifier(); + if (!callerName.equals(cleanFieldName)) { + String callerType = getVariableDeclaredType(methodFqn, callerName); + if (callerType != null && callerType.contains("<")) { + int start = callerType.indexOf('<'); + int end = callerType.lastIndexOf('>'); + if (start >= 0 && end > start) { + foundType[0] = callerType.substring(start + 1, end); + return false; + } + } + } + } + } + } + } + } + return super.visit(node); + } + }); + } + if (foundType[0] != null) return foundType[0]; + } + + // Fallback to fields + for (FieldDeclaration fd : td.getFields()) { + for (Object fragObj : fd.fragments()) { + VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(cleanFieldName)) { + return fd.getType().toString(); + } + } + } + } + } + + // Final fallback: ask constantExtractor to resolve it via method return constant + if (constantExtractor != null && methodFqn.contains(".")) { + String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); + String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); + TypeDeclaration td = context.getTypeDeclaration(className); + CompilationUnit cu = td != null && td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + List values = constantExtractor.resolveMethodReturnConstant(cleanFieldName, methodName, 0, new HashSet<>(), cu); + if (values != null && !values.isEmpty()) { + // If it resolves to some class constant, we could infer type or return a fallback + return null; + } + } + return null; + } + + public String traceLocalVariable(String methodFqn, String varName) { + 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) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && md.getBody() != null) { + List initializers = new ArrayList<>(); + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationFragment node) { + if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { + initializers.add(node.getInitializer()); + } + return super.visit(node); + } + @Override + public boolean visit(Assignment node) { + if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { + initializers.add(node.getRightHandSide()); + } + return super.visit(node); + } + }); + + if (!initializers.isEmpty()) { + List stringified = new ArrayList<>(); + for (Expression expr : initializers) { + Expression traced = traceVariable(expr); + if (traced instanceof MethodInvocation mi) { + Expression innerMost = unwrapMethodInvocation(mi, 0); + if (innerMost instanceof MethodInvocation innerMi) { + if (innerMi.getExpression() instanceof SimpleName sn) { + stringified.add(sn.getIdentifier() + "." + innerMi.getName().getIdentifier() + "()"); + } else { + stringified.add(innerMi.getName().getIdentifier() + "()"); + } + } else if (innerMost instanceof SimpleName sn) { + stringified.add(sn.getIdentifier()); + } else { + stringified.add(innerMost.toString()); + } + } else if (traced instanceof SimpleName sn) { + stringified.add(sn.getIdentifier()); + } else if (traced instanceof ArrayInitializer) { + stringified.add("new Object[]" + traced.toString()); + } else { + stringified.add(traced.toString()); + } + } + if (stringified.size() == 1) return stringified.get(0); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < stringified.size() - 1; i++) { + sb.append("true ? ").append(stringified.get(i)).append(" : "); + } + sb.append(stringified.get(stringified.size() - 1)); + return sb.toString(); + } + } + + // If local variable not found, check field assignments + String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName; + final Expression[] fieldInitializer = new Expression[1]; + ASTVisitor fieldVisitor = new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationFragment node) { + if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) { + fieldInitializer[0] = node.getInitializer(); + } + return super.visit(node); + } + @Override + public boolean visit(Assignment node) { + Expression left = node.getLeftHandSide(); + if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) || + (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) { + fieldInitializer[0] = node.getRightHandSide(); + } + return super.visit(node); + } + }; + + for (FieldDeclaration fd : td.getFields()) { + for (Object fragObj : fd.fragments()) { + VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) { + fieldInitializer[0] = frag.getInitializer(); + } + } + } + if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); + + for (MethodDeclaration classMd : td.getMethods()) { + if (classMd.isConstructor() && classMd.getBody() != null) { + classMd.getBody().accept(fieldVisitor); + } + } + if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); + + for (MethodDeclaration classMd : td.getMethods()) { + if (!classMd.isConstructor() && classMd.getBody() != null) { + classMd.getBody().accept(fieldVisitor); + } + } + if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); + } + return null; + } + + public String traceLocalVariable(String methodFqn, String varName, Map parameterValues) { + String result = traceLocalVariable(methodFqn, varName); + if (result != null && parameterValues != null && !parameterValues.isEmpty()) { + TypeDeclaration td = context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))); + if (td != null) { + String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && md.getBody() != null) { + final ASTNode[] switchNode = new ASTNode[1]; + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationFragment node) { + if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { + Expression init = node.getInitializer(); + if (init.getNodeType() == ASTNode.SWITCH_EXPRESSION) { + switchNode[0] = init; + return false; + } + if (init.getNodeType() == ASTNode.SWITCH_STATEMENT) { + switchNode[0] = init; + return false; + } + } + return super.visit(node); + } + @Override + public boolean visit(Assignment node) { + if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { + Expression rhs = node.getRightHandSide(); + if (rhs.getNodeType() == ASTNode.SWITCH_EXPRESSION) { + switchNode[0] = rhs; + return false; + } + if (rhs.getNodeType() == ASTNode.SWITCH_STATEMENT) { + switchNode[0] = rhs; + return false; + } + } + return super.visit(node); + } + }); + if (switchNode[0] != null) { + if (switchNode[0] instanceof SwitchExpression se) { + String evaluated = constantResolver.evaluateSwitchWithParams(se, parameterValues, context); + if (evaluated != null) return evaluated; + } else if (switchNode[0] instanceof SwitchStatement ss) { + String evaluated = constantResolver.evaluateSwitchWithParams(ss, parameterValues, context); + if (evaluated != null) return evaluated; + } + } + } + } + } + return result; + } + + public Map buildParameterValuesMap(String caller, String target, Map> callGraph, List path, int pathIndex) { + Map paramValues = new HashMap<>(); + List edges = callGraph.get(caller); + if (edges == null) { + return paramValues; + } + + // We also want to support heuristic matches using a helper if isHeuristicMatch is not directly available, but let's check + // We can check if name equals or if it resolves to target + for (CallEdge edge : edges) { + if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { + List args = edge.getArguments(); + if (args == null || args.isEmpty()) break; + + int lastDot = target.lastIndexOf('.'); + if (lastDot < 0) break; + String className = target.substring(0, lastDot); + String methodName = target.substring(lastDot + 1); + + TypeDeclaration td = context.getTypeDeclaration(className); + if (td == null) break; + + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md == null) break; + + List paramNames = new ArrayList<>(); + for (Object paramObj : md.parameters()) { + SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj; + paramNames.add(param.getName().getIdentifier()); + } + + int minSize = Math.min(paramNames.size(), args.size()); + for (int j = 0; j < minSize; j++) { + String argValue = args.get(j); + String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph); + paramValues.put(paramNames.get(j), resolvedArgValue); + } + break; + } + } + return paramValues; + } + + public String resolveArgumentValue(String argValue, String callerMethod, List path, int pathIndex, Map> callGraph) { + if (argValue == null) return null; + if (argValue.contains(".") || argValue.startsWith("new ") || argValue.matches(".*[0-9].*")) { + return argValue; + } + + int callerIndex = path.indexOf(callerMethod); + if (callerIndex <= 0) return argValue; + + String prevCaller = path.get(callerIndex - 1); + List prevEdges = callGraph.get(prevCaller); + if (prevEdges == null) return argValue; + + for (CallEdge edge : prevEdges) { + if (edge.getTargetMethod().equals(callerMethod) || isHeuristicMatch(edge.getTargetMethod(), callerMethod)) { + List prevArgs = edge.getArguments(); + if (prevArgs == null || prevArgs.isEmpty()) break; + + int lastDot = callerMethod.lastIndexOf('.'); + if (lastDot < 0) break; + String className = callerMethod.substring(0, lastDot); + String methodName = callerMethod.substring(lastDot + 1); + + TypeDeclaration td = context.getTypeDeclaration(className); + if (td == null) break; + + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md == null) break; + + List paramNames = new ArrayList<>(); + for (Object paramObj : md.parameters()) { + SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj; + paramNames.add(param.getName().getIdentifier()); + } + + int paramIdx = paramNames.indexOf(argValue); + if (paramIdx >= 0 && paramIdx < prevArgs.size()) { + String prevArg = prevArgs.get(paramIdx); + return resolveArgumentValue(prevArg, prevCaller, path, pathIndex, callGraph); + } + break; + } + } + return argValue; + } + + private boolean isHeuristicMatch(String neighbor, String target) { + if (neighbor == null || target == null) return false; + if (neighbor.equals(target)) return true; + + if (neighbor.contains(".") && target.contains(".")) { + String methodNeighbor = neighbor.substring(neighbor.lastIndexOf('.') + 1); + String methodTarget = target.substring(target.lastIndexOf('.') + 1); + if (!methodNeighbor.equals(methodTarget)) return false; + + String classNeighbor = neighbor.substring(0, neighbor.lastIndexOf('.')); + String classTarget = target.substring(0, target.lastIndexOf('.')); + if (classNeighbor.equals(classTarget)) return true; + + String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor; + String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget; + if (simpleClassNeighbor.equals(simpleClassTarget)) return true; + + List impls = context.getImplementations(classTarget); + if (impls != null && impls.contains(classNeighbor)) return true; + + List implsN = context.getImplementations(classNeighbor); + if (implsN != null && implsN.contains(classTarget)) return true; + } + return false; + } + + private MethodDeclaration findEnclosingMethod(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof MethodDeclaration)) { + parent = parent.getParent(); + } + return (MethodDeclaration) parent; + } + + private TypeDeclaration findEnclosingType(ASTNode node) { + ASTNode parent = node.getParent(); + while (parent != null && !(parent instanceof TypeDeclaration)) { + parent = parent.getParent(); + } + return (TypeDeclaration) parent; + } + + private Expression unwrapMethodInvocation(MethodInvocation mi, int depth) { + if (depth > 5) return mi; + if (mi.getExpression() != null) { + String exprStr = mi.getExpression().toString(); + if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) { + return mi; + } + } + + if (!mi.arguments().isEmpty()) { + String mName = mi.getName().getIdentifier(); + String lowerName = mName.toLowerCase(); + + if (lowerName.startsWith("map") || lowerName.startsWith("parse") || + lowerName.startsWith("convert") || lowerName.startsWith("to") || + lowerName.startsWith("resolve") || lowerName.startsWith("build") || + lowerName.startsWith("create") || lowerName.startsWith("from") || + lowerName.startsWith("transform") || lowerName.startsWith("translate") || + lowerName.startsWith("derive") || lowerName.startsWith("determine") || + lowerName.startsWith("calculate") || lowerName.startsWith("decode") || + lowerName.startsWith("extract") || lowerName.startsWith("get") || + lowerName.startsWith("find")) { + return mi; + } + + Expression arg = (Expression) mi.arguments().get(0); + if (arg instanceof MethodInvocation innerMi) { + return unwrapMethodInvocation(innerMi, depth + 1); + } + return arg; + } + return mi; + } +}