heuristics updated enum collision
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
System.out.println("DEBUG evaluateGetterOnReceiver: resolvedValue=" + resolvedValue + " methodSuffix=" + methodSuffix + " entryMethod=" + entryMethod);
|
||||
org.eclipse.jdt.core.dom.ASTParser exprParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17);
|
||||
exprParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION);
|
||||
exprParser.setSource(resolvedValue.toCharArray());
|
||||
@@ -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<String, String> fieldValues = new HashMap<>();
|
||||
String varName = receiverName;
|
||||
org.eclipse.jdt.core.dom.Expression initExpr = findVariableInitializer(entryMethod, varName);
|
||||
System.out.println("DEBUG evaluateGetterOnReceiver: initExpr=" + initExpr + " class=" + (initExpr != null ? initExpr.getClass().getName() : "null"));
|
||||
if (initExpr instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) {
|
||||
fieldValues.putAll(buildFieldValuesFromCIC(td, cic, context, 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(".")) {
|
||||
|
||||
@@ -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<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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user