heuristics updated enum collision

This commit is contained in:
2026-06-25 06:39:31 +02:00
parent 75fd45d983
commit b45513c5fb
4 changed files with 200 additions and 25 deletions

View File

@@ -310,6 +310,11 @@ public class ConstantResolver {
String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues); String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues);
if (switchVar == null) return null; if (switchVar == null) return null;
String switchType = null;
if (switchVar.contains(".")) {
switchType = switchVar.substring(0, switchVar.lastIndexOf('.'));
}
boolean matchingCase = false; boolean matchingCase = false;
for (Object stmtObj : ss.statements()) { for (Object stmtObj : ss.statements()) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) { if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
@@ -322,11 +327,21 @@ public class ConstantResolver {
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback // Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) { if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
caseVal = caseSn.getIdentifier(); caseVal = caseSn.getIdentifier();
if (switchType != null) {
caseVal = switchType + "." + caseVal;
} }
if (caseVal != null && switchVar.endsWith(caseVal)) { }
if (caseVal != null) {
if (switchVar.contains(".") && caseVal.contains(".")) {
if (switchVar.equals(caseVal)) {
matchingCase = true; matchingCase = true;
break; break;
} }
} else if (switchVar.endsWith(caseVal)) {
matchingCase = true;
break;
}
}
} }
} }
} else if (matchingCase) { } else if (matchingCase) {
@@ -346,6 +361,11 @@ public class ConstantResolver {
String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues); String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues);
if (switchVar == null) return null; if (switchVar == null) return null;
String switchType = null;
if (switchVar.contains(".")) {
switchType = switchVar.substring(0, switchVar.lastIndexOf('.'));
}
boolean matchingCase = false; boolean matchingCase = false;
for (Object stmtObj : se.statements()) { for (Object stmtObj : se.statements()) {
if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) { if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) {
@@ -358,11 +378,21 @@ public class ConstantResolver {
// Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback // Bare SimpleName case label (e.g. "case PAY"): use identifier as fallback
if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) { if (caseVal == null && exprObj instanceof org.eclipse.jdt.core.dom.SimpleName caseSn) {
caseVal = caseSn.getIdentifier(); caseVal = caseSn.getIdentifier();
if (switchType != null) {
caseVal = switchType + "." + caseVal;
} }
if (caseVal != null && switchVar.endsWith(caseVal)) { }
if (caseVal != null) {
if (switchVar.contains(".") && caseVal.contains(".")) {
if (switchVar.equals(caseVal)) {
matchingCase = true; matchingCase = true;
break; break;
} }
} else if (switchVar.endsWith(caseVal)) {
matchingCase = true;
break;
}
}
} }
} }
} else if (matchingCase) { } else if (matchingCase) {

View File

@@ -100,6 +100,23 @@ import java.util.*;
if (edges != null) { if (edges != null) {
for (CallEdge edge : edges) { for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { 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()) { if (paramIndex < edge.getArguments().size()) {
String arg = edge.getArguments().get(paramIndex); String arg = edge.getArguments().get(paramIndex);
if (arg != null) { if (arg != null) {
@@ -150,7 +167,6 @@ import java.util.*;
} }
String tracedVar = traceLocalVariable(entryMethod, currentParamName); String tracedVar = traceLocalVariable(entryMethod, currentParamName);
System.out.println("DEBUG tracedVar: " + tracedVar + " for currentParamName: " + currentParamName);
if (tracedVar != null && !tracedVar.equals(currentParamName)) { if (tracedVar != null && !tracedVar.equals(currentParamName)) {
String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix); String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix);
tracedVar = extractedFinalTraced[0]; tracedVar = extractedFinalTraced[0];
@@ -181,12 +197,10 @@ import java.util.*;
} }
// Parse resolvedValue using JDT to robustly handle complex expressions // 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); 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.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
exprParser.setSource(resolvedValue.toCharArray()); exprParser.setSource(resolvedValue.toCharArray());
org.eclipse.jdt.core.dom.ASTNode exprNode = exprParser.createAST(null); 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 varName = null;
String methodName = null; String methodName = null;
@@ -530,18 +544,50 @@ import java.util.*;
return -1; 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) { public String getVariableDeclaredType(String methodFqn, String cleanFieldName) {
System.out.println("DEBUG getVariableDeclaredType called with: " + methodFqn + ", " + cleanFieldName);
if (methodFqn == null) return null; if (methodFqn == null) return null;
int lastDot = methodFqn.lastIndexOf('.'); int lastDot = methodFqn.lastIndexOf('.');
if (lastDot > 0) { if (lastDot > 0) {
String fqn = methodFqn.substring(0, lastDot); String fqn = methodFqn.substring(0, lastDot);
String methodName = methodFqn.substring(lastDot + 1); String methodName = methodFqn.substring(lastDot + 1);
TypeDeclaration td = context.getTypeDeclaration(fqn); TypeDeclaration td = context.getTypeDeclaration(fqn);
System.out.println("DEBUG getTypeDeclaration for " + fqn + " = " + (td != null));
if (td != null) { if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, false); MethodDeclaration md = context.findMethodDeclaration(td, methodName, false);
System.out.println("DEBUG findMethodDeclaration for " + methodName + " = " + (md != null));
if (md != null) { if (md != null) {
for (Object pObj : md.parameters()) { for (Object pObj : md.parameters()) {
SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj; SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj;
@@ -551,7 +597,6 @@ import java.util.*;
} }
final String[] foundType = new String[1]; final String[] foundType = new String[1];
if (md.getBody() != null) { if (md.getBody() != null) {
System.out.println("DEBUG visiting body for " + cleanFieldName);
md.getBody().accept(new ASTVisitor() { md.getBody().accept(new ASTVisitor() {
@Override @Override
public boolean visit(VariableDeclarationStatement node) { public boolean visit(VariableDeclarationStatement node) {
@@ -568,12 +613,9 @@ import java.util.*;
public boolean visit(org.eclipse.jdt.core.dom.LambdaExpression node) { public boolean visit(org.eclipse.jdt.core.dom.LambdaExpression node) {
for (Object paramObj : node.parameters()) { for (Object paramObj : node.parameters()) {
org.eclipse.jdt.core.dom.VariableDeclaration param = (org.eclipse.jdt.core.dom.VariableDeclaration) paramObj; 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)) { if (param.getName().getIdentifier().equals(cleanFieldName)) {
System.out.println("DEBUG found lambda param: " + cleanFieldName);
if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) { if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) {
foundType[0] = svd.getType().toString(); foundType[0] = svd.getType().toString();
System.out.println("DEBUG lambda explicit type: " + foundType[0]);
return false; return false;
} }
@@ -1903,7 +1945,15 @@ import java.util.*;
} }
protected boolean isHeuristicMatch(String neighbor, String target) { 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 simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor; String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
return simpleNeighbor.equals(simpleTarget); return simpleNeighbor.equals(simpleTarget);

View File

@@ -582,10 +582,27 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
if (edges != null) { if (edges != null) {
for (CallEdge edge : edges) { for (CallEdge edge : edges) {
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) { 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()) { if (paramIndex < edge.getArguments().size()) {
String arg = edge.getArguments().get(paramIndex); String arg = edge.getArguments().get(paramIndex);
if (arg != null) { if (arg != null) {
System.out.println("DEBUG TRACE: caller=" + caller + " target=" + target + " paramIndex=" + paramIndex + " arg=" + arg + " receiver=" + edge.getReceiver());
String receiver = edge.getReceiver(); String receiver = edge.getReceiver();
if (receiver != null && !receiver.isEmpty() && ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super."))) { if (receiver != null && !receiver.isEmpty() && ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super."))) {
String actualReceiver = receiver; String actualReceiver = receiver;
@@ -738,7 +755,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} }
private List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) { private List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
System.out.println("DEBUG evaluateGetterOnReceiver: resolvedValue=" + resolvedValue + " methodSuffix=" + methodSuffix + " entryMethod=" + entryMethod);
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17); 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.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
exprParser.setSource(resolvedValue.toCharArray()); exprParser.setSource(resolvedValue.toCharArray());
@@ -753,13 +769,11 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
if (receiver instanceof org.eclipse.jdt.core.dom.SimpleName sn) { if (receiver instanceof org.eclipse.jdt.core.dom.SimpleName sn) {
receiverName = sn.getIdentifier(); receiverName = sn.getIdentifier();
receiverType = getVariableDeclaredType(entryMethod, receiverName); receiverType = getVariableDeclaredType(entryMethod, receiverName);
System.out.println("DEBUG evaluateGetterOnReceiver: receiverName=" + receiverName + " receiverType=" + receiverType);
} }
if (receiverType == null && path.size() > 1) { if (receiverType == null && path.size() > 1) {
String callerMethod = path.get(1); String callerMethod = path.get(1);
receiverType = getVariableDeclaredType(callerMethod, receiverName); receiverType = getVariableDeclaredType(callerMethod, receiverName);
System.out.println("DEBUG evaluateGetterOnReceiver: fallback receiverType=" + receiverType);
} }
if (receiverType != null) { if (receiverType != null) {
@@ -767,7 +781,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
if (receiverType.contains("<")) { if (receiverType.contains("<")) {
simpleReceiverType = receiverType.substring(0, receiverType.indexOf('<')); simpleReceiverType = receiverType.substring(0, receiverType.indexOf('<'));
} }
System.out.println("DEBUG evaluateGetterOnReceiver: simpleReceiverType=" + simpleReceiverType);
TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType); TypeDeclaration td = context.getTypeDeclaration(simpleReceiverType);
if (td != null) { if (td != null) {
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true); MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
@@ -775,10 +788,8 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
Map<String, String> fieldValues = new HashMap<>(); Map<String, String> fieldValues = new HashMap<>();
String varName = receiverName; String varName = receiverName;
org.eclipse.jdt.core.dom.Expression initExpr = findVariableInitializer(entryMethod, varName); 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) { if (initExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor)); fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor));
System.out.println("DEBUG evaluateGetterOnReceiver: fieldValues=" + fieldValues);
int lastDot = entryMethod.lastIndexOf('.'); int lastDot = entryMethod.lastIndexOf('.');
if (lastDot > 0) { if (lastDot > 0) {
String className = entryMethod.substring(0, lastDot); String className = entryMethod.substring(0, lastDot);
@@ -793,7 +804,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} }
} }
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>()); String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
System.out.println("DEBUG evaluateGetterOnReceiver: result=" + result);
if (result != null) { if (result != null) {
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null; String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
if (returnType != null && !result.contains(".")) { if (returnType != null && !result.contains(".")) {

View File

@@ -227,4 +227,89 @@ class HeuristicCallGraphEngineExtendedTest {
.containsExactlyInAnyOrder("TransitionEnum.CHECKOUT_STARTED"); .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<CallChain> 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<CallChain> 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");
}
} }