This commit is contained in:
2026-06-22 13:53:27 +02:00
parent c6b4a4be1e
commit 3ca834fa71
5 changed files with 201 additions and 12 deletions

View File

@@ -88,6 +88,13 @@ public class ConstantResolver {
} }
} }
if ("valueOf".equals(mi.getName().getIdentifier()) && mi.getExpression() != null) {
java.util.List<String> enumValues = context.getEnumValues(mi.getExpression().toString());
if (enumValues != null && !enumValues.isEmpty()) {
return "ENUM_SET:" + String.join(",", enumValues);
}
}
IMethodBinding mb = mi.resolveMethodBinding(); IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) { if (mb != null) {
ITypeBinding returnType = mb.getReturnType(); ITypeBinding returnType = mb.getReturnType();

View File

@@ -316,6 +316,28 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (staticTd != null) { if (staticTd != null) {
declaredType = context.getFqn(staticTd); declaredType = context.getFqn(staticTd);
sourceMethod = "static-call"; sourceMethod = "static-call";
} else if (context.getEnumValues(varName) != null) {
declaredType = varName;
sourceMethod = "static-call";
}
}
}
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<String> values = resolveMethodReturnConstant(context.getFqn(currentTd), methodName, 0, new HashSet<>(), cu);
System.out.println("DEBUG fallback resolveMethodReturnConstant returned: " + values);
if (values != null && !values.isEmpty()) {
polymorphicEvents.addAll(values);
sourceMethod = methodFqn;
break;
}
} }
} }
} }
@@ -391,8 +413,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
// Clean up any remaining invalid AST fallbacks (like 'type', 'event', or unresolved class names) // Clean up any remaining invalid AST fallbacks (like 'type', 'event', or unresolved class names)
polymorphicEvents.removeIf(e -> { polymorphicEvents.removeIf(e -> {
String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e; if (e.contains(".")) return false; // If it's a fully qualified enum/constant, keep it
return !val.equals(val.toUpperCase()) || val.length() <= 2; String val = e;
return !val.equals(val.toUpperCase()) || val.length() <= 1;
}); });
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList()); polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
@@ -832,8 +855,23 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
} }
} else { } else {
for (Object argObj : cic.arguments()) { boolean handledByLombok = false;
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants); 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) {
for (Object argObj : cic.arguments()) {
extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants);
}
} }
} }
} }
@@ -859,6 +897,61 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
} }
} }
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;
}
private void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List<String> constants) { private void extractConstantsFromArgument(org.eclipse.jdt.core.dom.Expression argObj, List<String> constants) {
if (argObj instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation innerCic) { if (argObj instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation innerCic) {
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType()); String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
@@ -1038,14 +1131,26 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
protected Expression unwrapMethodInvocation(org.eclipse.jdt.core.dom.MethodInvocation mi, int depth) { protected Expression unwrapMethodInvocation(org.eclipse.jdt.core.dom.MethodInvocation mi, int depth) {
if (depth > 5) return mi; if (depth > 5) return mi;
// Do not unwrap if called on an object (e.g. mapper.toEvent) // Do not unwrap if called on an object (e.g. mapper.toEvent)
// Exceptions: Optional.of, Optional.ofNullable, etc. // Exceptions: Optional.of, Optional.ofNullable, Stream, etc.
if (mi.getExpression() != null) { if (mi.getExpression() != null) {
String exprStr = mi.getExpression().toString(); String exprStr = mi.getExpression().toString();
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays")) { if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects")) {
return mi; return mi;
} }
} }
if (!mi.arguments().isEmpty()) { 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")) {
return mi;
}
Expression arg = (Expression) mi.arguments().get(0); Expression arg = (Expression) mi.arguments().get(0);
if (arg instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) { if (arg instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) {
return unwrapMethodInvocation(innerMi, depth + 1); return unwrapMethodInvocation(innerMi, depth + 1);

View File

@@ -45,7 +45,14 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
if (td2 != null) { if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier()); String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) { if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args)); List<String> implicitArgs = new ArrayList<>();
String invokedMethod = node.getName().getIdentifier();
if (node.getExpression() != null && (invokedMethod.equals("forEach") || invokedMethod.equals("map") || invokedMethod.equals("filter") || invokedMethod.equals("flatMap"))) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
} }
} }
} else { } else {
@@ -58,7 +65,14 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
} }
if (fallbackTypeFqn != null) { if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); List<String> implicitArgs = new ArrayList<>();
String invokedMethod = node.getName().getIdentifier();
if (node.getExpression() != null && (invokedMethod.equals("forEach") || invokedMethod.equals("map") || invokedMethod.equals("filter") || invokedMethod.equals("flatMap"))) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
List<String> impls = context.getImplementations(fallbackTypeFqn); List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) { for (String impl : impls) {

View File

@@ -49,7 +49,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
if (td2 != null) { if (td2 != null) {
String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier()); String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier());
if (refMethod != null) { if (refMethod != null) {
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args)); List<String> implicitArgs = new ArrayList<>();
String invokedMethod = node.getName().getIdentifier();
if (node.getExpression() != null && (invokedMethod.equals("forEach") || invokedMethod.equals("map") || invokedMethod.equals("filter") || invokedMethod.equals("flatMap"))) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, implicitArgs));
} }
} }
} else { } else {
@@ -62,7 +69,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
} }
if (fallbackTypeFqn != null) { if (fallbackTypeFqn != null) {
String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier();
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); List<String> implicitArgs = new ArrayList<>();
String invokedMethod = node.getName().getIdentifier();
if (node.getExpression() != null && (invokedMethod.equals("forEach") || invokedMethod.equals("map") || invokedMethod.equals("filter") || invokedMethod.equals("flatMap"))) {
implicitArgs.add(node.getExpression().toString());
} else {
implicitArgs.addAll(args);
}
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, implicitArgs));
List<String> impls = context.getImplementations(fallbackTypeFqn); List<String> impls = context.getImplementations(fallbackTypeFqn);
for (String impl : impls) { for (String impl : impls) {

View File

@@ -2704,4 +2704,53 @@ class HeuristicCallGraphEngineTest {
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactly("CORRECT_EVENT"); .containsExactly("CORRECT_EVENT");
} }
@Test
void shouldResolveEnumValueOfFromString(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class Endpoint {
private OrderService service;
public void trigger(String eventString) {
service.doSomething(eventString);
}
}
class OrderService {
public void doSomething(String str) {
MyEvents event = mapToEnum(str);
sendEvent(event);
}
private MyEvents mapToEnum(String str) {
return MyEvents.valueOf(str);
}
public void sendEvent(MyEvents e) {}
}
enum MyEvents { A, B }
""";
Files.writeString(tempDir.resolve("Endpoint.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.Endpoint")
.methodName("trigger")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("sendEvent")
.event("e")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("MyEvents.A", "MyEvents.B");
}
} }