diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java index 45cf5c6..dc89109 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java @@ -310,6 +310,11 @@ public class ConstantResolver { String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues); if (switchVar == null) return null; + String switchType = null; + if (switchVar.contains(".")) { + switchType = switchVar.substring(0, switchVar.lastIndexOf('.')); + } + boolean matchingCase = false; for (Object stmtObj : ss.statements()) { if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) { @@ -322,10 +327,20 @@ public class ConstantResolver { // Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) { caseVal = caseSn.getIdentifier(); + if (switchType != null) { + caseVal = switchType + "." + caseVal; + } } - if (caseVal != null && switchVar.endsWith(caseVal)) { - matchingCase = true; - break; + if (caseVal != null) { + if (switchVar.contains(".") && caseVal.contains(".")) { + if (switchVar.equals(caseVal)) { + matchingCase = true; + break; + } + } else if (switchVar.endsWith(caseVal)) { + matchingCase = true; + break; + } } } } @@ -346,6 +361,11 @@ public class ConstantResolver { String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues); if (switchVar == null) return null; + String switchType = null; + if (switchVar.contains(".")) { + switchType = switchVar.substring(0, switchVar.lastIndexOf('.')); + } + boolean matchingCase = false; for (Object stmtObj : se.statements()) { if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) { @@ -358,10 +378,20 @@ public class ConstantResolver { // Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) { caseVal = caseSn.getIdentifier(); + if (switchType != null) { + caseVal = switchType + "." + caseVal; + } } - if (caseVal != null && switchVar.endsWith(caseVal)) { - matchingCase = true; - break; + if (caseVal != null) { + if (switchVar.contains(".") && caseVal.contains(".")) { + if (switchVar.equals(caseVal)) { + matchingCase = true; + break; + } + } else if (switchVar.endsWith(caseVal)) { + matchingCase = true; + break; + } } } } 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 b0ebab1..137cc1a 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 @@ -100,6 +100,23 @@ import java.util.*; if (edges != null) { for (CallEdge edge : edges) { if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { + String expectedType = getParameterType(target, paramIndex); + if (expectedType != null && paramIndex < edge.getArguments().size()) { + String argValue = edge.getArguments().get(paramIndex); + String actualType = null; + if (argValue.contains(".") && !argValue.contains("(")) { + String prefix = argValue.substring(0, argValue.lastIndexOf('.')); + if (!prefix.equals("this") && !prefix.equals("super")) { + actualType = prefix; + } + } + if (actualType == null && !argValue.contains(".")) { + actualType = getVariableDeclaredType(caller, argValue); + } + if (actualType != null && !isTypeCompatible(actualType, expectedType)) { + continue; + } + } if (paramIndex < edge.getArguments().size()) { String arg = edge.getArguments().get(paramIndex); if (arg != null) { @@ -150,7 +167,6 @@ import java.util.*; } String tracedVar = traceLocalVariable(entryMethod, currentParamName); - System.out.println("DEBUG tracedVar: " + tracedVar + " for currentParamName: " + currentParamName); if (tracedVar != null && !tracedVar.equals(currentParamName)) { String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix); tracedVar = extractedFinalTraced[0]; @@ -181,12 +197,10 @@ import java.util.*; } // Parse resolvedValue using JDT to robustly handle complex expressions - System.out.println("DEBUG Parsing resolvedValue: " + resolvedValue); 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); - System.out.println("DEBUG exprNode: " + (exprNode != null ? exprNode.getClass().getName() : "null") + ", node: " + exprNode); String varName = null; String methodName = null; @@ -530,18 +544,50 @@ import java.util.*; 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 (actualType.equals(expectedType)) return true; + if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true; + + TypeDeclaration td = context.getTypeDeclaration(actualType); + if (td != null) { + String superFqn = context.getSuperclassFqn(td); + while (superFqn != null) { + if (superFqn.equals(expectedType) || superFqn.endsWith("." + expectedType)) return true; + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + superFqn = superTd != null ? context.getSuperclassFqn(superTd) : null; + } + for (Object interfaceType : td.superInterfaceTypes()) { + if (interfaceType.toString().equals(expectedType) || interfaceType.toString().endsWith("." + expectedType)) return true; + } + } + return false; + } + public String getVariableDeclaredType(String methodFqn, String cleanFieldName) { - System.out.println("DEBUG getVariableDeclaredType called with: " + methodFqn + ", " + 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); - System.out.println("DEBUG getTypeDeclaration for " + fqn + " = " + (td != null)); if (td != null) { MethodDeclaration md = context.findMethodDeclaration(td, methodName, false); - System.out.println("DEBUG findMethodDeclaration for " + methodName + " = " + (md != null)); if (md != null) { for (Object pObj : md.parameters()) { SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj; @@ -551,7 +597,6 @@ import java.util.*; } final String[] foundType = new String[1]; if (md.getBody() != null) { - System.out.println("DEBUG visiting body for " + cleanFieldName); md.getBody().accept(new ASTVisitor() { @Override public boolean visit(VariableDeclarationStatement node) { @@ -568,12 +613,9 @@ import java.util.*; 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; - System.out.println("DEBUG visiting lambda param: " + param.getName().getIdentifier()); if (param.getName().getIdentifier().equals(cleanFieldName)) { - System.out.println("DEBUG found lambda param: " + cleanFieldName); if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) { foundType[0] = svd.getType().toString(); - System.out.println("DEBUG lambda explicit type: " + foundType[0]); return false; } @@ -1903,7 +1945,15 @@ import java.util.*; } protected boolean isHeuristicMatch(String neighbor, String target) { - if (target.endsWith("." + neighbor)) return true; + if (neighbor == null || target == null) return false; + if (neighbor.equals(target)) return true; + + // If both have packages/classes, they must match more strictly + if (neighbor.contains(".") && target.contains(".")) { + return neighbor.endsWith(target) || target.endsWith(neighbor); + } + + // 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); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index dc31169..86966e1 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -582,10 +582,27 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { if (edges != null) { for (CallEdge edge : edges) { if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { + String expectedType = getParameterType(target, paramIndex); + if (expectedType != null && paramIndex < edge.getArguments().size()) { + String argValue = edge.getArguments().get(paramIndex); + String actualType = null; + if (argValue.contains(".") && !argValue.contains("(")) { + String prefix = argValue.substring(0, argValue.lastIndexOf('.')); + if (!prefix.equals("this") && !prefix.equals("super")) { + actualType = prefix; + } + } + if (actualType == null && !argValue.contains(".")) { + actualType = getVariableDeclaredType(caller, argValue); + } + if (actualType != null && !isTypeCompatible(actualType, expectedType)) { + log.debug("Call rejected due to type mismatch: {} calling {} with type {} instead of expected {}", caller, edge.getTargetMethod(), actualType, expectedType); + continue; + } + } if (paramIndex < edge.getArguments().size()) { String arg = edge.getArguments().get(paramIndex); if (arg != null) { - System.out.println("DEBUG TRACE: caller=" + caller + " target=" + target + " paramIndex=" + paramIndex + " arg=" + arg + " receiver=" + edge.getReceiver()); String receiver = edge.getReceiver(); if (receiver != null && !receiver.isEmpty() && ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super."))) { String actualReceiver = receiver; @@ -738,7 +755,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { } private List evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List path, Map> callGraph) { - System.out.println("DEBUG evaluateGetterOnReceiver: resolvedValue=" + resolvedValue + " methodSuffix=" + methodSuffix + " entryMethod=" + entryMethod); org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17); exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION); exprParser.setSource(resolvedValue.toCharArray()); @@ -753,13 +769,11 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { if (receiver instanceof org.eclipse.jdt.core.dom.SimpleName sn) { receiverName = sn.getIdentifier(); receiverType = getVariableDeclaredType(entryMethod, receiverName); - System.out.println("DEBUG evaluateGetterOnReceiver: receiverName=" + receiverName + " receiverType=" + receiverType); } if (receiverType == null && path.size() > 1) { String callerMethod = path.get(1); receiverType = getVariableDeclaredType(callerMethod, receiverName); - System.out.println("DEBUG evaluateGetterOnReceiver: fallback receiverType=" + receiverType); } if (receiverType != null) { @@ -767,7 +781,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { if (receiverType.contains("<")) { simpleReceiverType = receiverType.substring(0, receiverType.indexOf('<')); } - System.out.println("DEBUG evaluateGetterOnReceiver: simpleReceiverType=" + simpleReceiverType); TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType); if (td != null) { MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true); @@ -775,10 +788,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { Map fieldValues = new HashMap<>(); String varName = receiverName; org.eclipse.jdt.core.dom.Expression initExpr = findVariableInitializer(entryMethod, varName); - System.out.println("DEBUG evaluateGetterOnReceiver: initExpr=" + initExpr + " class=" + (initExpr != null ? initExpr.getClass().getName() : "null")); if (initExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor)); - System.out.println("DEBUG evaluateGetterOnReceiver: fieldValues=" + fieldValues); int lastDot = entryMethod.lastIndexOf('.'); if (lastDot > 0) { String className = entryMethod.substring(0, lastDot); @@ -793,7 +804,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { } } String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>()); - System.out.println("DEBUG evaluateGetterOnReceiver: result=" + result); if (result != null) { String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null; if (returnType != null && !result.contains(".")) { diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineExtendedTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineExtendedTest.java index 378b11f..e8df2e6 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineExtendedTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineExtendedTest.java @@ -227,4 +227,89 @@ class HeuristicCallGraphEngineExtendedTest { .containsExactlyInAnyOrder("TransitionEnum.CHECKOUT_STARTED"); } + @Test + void shouldResolveCorrectEnumWhenNamesCollide() throws IOException { + String source = """ + package com.example; + + public class OrderService { + private StateMachine machine; + + public void processOrder() { + // We use OrderEvent.PROCESS + machine.fire(OrderEvent.PROCESS); + } + } + + public class InventoryService { + private StateMachine machine; + + public void updateInventory() { + // We use InternalEvent.PROCESS which has same simple name + machine.fireInternal(InternalEvent.PROCESS); + } + + public void wrongProcess() { + // This calls fire(OrderEvent) but with InternalEvent.PROCESS + // It should be REJECTED by type-aware heuristic matching if it matches fire(OrderEvent) + machine.fire(InternalEvent.PROCESS); + } + } + + class StateMachine { + // Two overloaded methods or methods in different classes (simulated by names) + public void fire(OrderEvent event) {} + public void fireInternal(InternalEvent event) {} + } + + enum OrderEvent { PROCESS, CANCEL } + enum InternalEvent { PROCESS, AUDIT } + """; + + java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_collision"); + java.nio.file.Files.writeString(tempDir.resolve("CollisionConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + // Scenario 1: Trace OrderService.processOrder + EntryPoint entryPoint1 = EntryPoint.builder() + .className("com.example.OrderService") + .methodName("processOrder") + .build(); + + TriggerPoint trigger1 = TriggerPoint.builder() + .className("com.example.StateMachine") + .methodName("fire") + .event("event") + .build(); + + List chains1 = engine.findChains(List.of(entryPoint1), List.of(trigger1)); + + assertThat(chains1).hasSize(1); + assertThat(chains1.get(0).getTriggerPoint().getPolymorphicEvents()) + .containsExactly("OrderEvent.PROCESS") + .doesNotContain("InternalEvent.PROCESS"); + + // Scenario 2: Trace InventoryService.updateInventory + EntryPoint entryPoint2 = EntryPoint.builder() + .className("com.example.InventoryService") + .methodName("updateInventory") + .build(); + + TriggerPoint trigger2 = TriggerPoint.builder() + .className("com.example.StateMachine") + .methodName("fireInternal") + .event("event") + .build(); + + List chains2 = engine.findChains(List.of(entryPoint2), List.of(trigger2)); + + assertThat(chains2).hasSize(1); + assertThat(chains2.get(0).getTriggerPoint().getPolymorphicEvents()) + .containsExactly("InternalEvent.PROCESS") + .doesNotContain("OrderEvent.PROCESS"); + } }