From 3ca834fa716fbe9422bf2d577133068ebbb71405 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Mon, 22 Jun 2026 13:53:27 +0200 Subject: [PATCH] fixes --- .../analysis/resolver/ConstantResolver.java | 9 +- .../service/AbstractCallGraphEngine.java | 117 +++++++++++++++++- .../service/HeuristicCallGraphEngine.java | 18 ++- .../analysis/service/JdtCallGraphEngine.java | 18 ++- .../service/HeuristicCallGraphEngineTest.java | 51 +++++++- 5 files changed, 201 insertions(+), 12 deletions(-) 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 739713c..43d8bed 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 @@ -87,7 +87,14 @@ public class ConstantResolver { return sn.getIdentifier(); } } - + + if ("valueOf".equals(mi.getName().getIdentifier()) && mi.getExpression() != null) { + java.util.List enumValues = context.getEnumValues(mi.getExpression().toString()); + if (enumValues != null && !enumValues.isEmpty()) { + return "ENUM_SET:" + String.join(",", enumValues); + } + } + IMethodBinding mb = mi.resolveMethodBinding(); if (mb != null) { ITypeBinding returnType = mb.getReturnType(); 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 75af825..f559bc8 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 @@ -316,6 +316,28 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { if (staticTd != null) { declaredType = context.getFqn(staticTd); 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 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) polymorphicEvents.removeIf(e -> { - String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e; - return !val.equals(val.toUpperCase()) || val.length() <= 2; + if (e.contains(".")) return false; // If it's a fully qualified enum/constant, keep it + String val = e; + return !val.equals(val.toUpperCase()) || val.length() <= 1; }); polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList()); @@ -832,8 +855,23 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } } } else { - for (Object argObj : cic.arguments()) { - extractConstantsFromArgument((org.eclipse.jdt.core.dom.Expression) argObj, constants); + 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) { + 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 constants) { if (argObj instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation innerCic) { 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) { if (depth > 5) return mi; // 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) { 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; } } + 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); if (arg instanceof org.eclipse.jdt.core.dom.MethodInvocation innerMi) { return unwrapMethodInvocation(innerMi, depth + 1); 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 46c04ce..4931739 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 @@ -45,7 +45,14 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { if (td2 != null) { String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier()); if (refMethod != null) { - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args)); + List 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 { @@ -58,7 +65,14 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { } if (fallbackTypeFqn != null) { String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); + List 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 impls = context.getImplementations(fallbackTypeFqn); for (String impl : impls) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index 9bb704b..2700b0a 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -49,7 +49,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { if (td2 != null) { String refMethod = resolveMethodInType(td2, emr.getName().getIdentifier()); if (refMethod != null) { - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(refMethod, args)); + List 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 { @@ -62,7 +69,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { } if (fallbackTypeFqn != null) { String calledMethod = fallbackTypeFqn + "." + emr.getName().getIdentifier(); - graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args)); + List 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 impls = context.getImplementations(fallbackTypeFqn); for (String impl : impls) { diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java index cf46251..c5558eb 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java @@ -2704,4 +2704,53 @@ class HeuristicCallGraphEngineTest { org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) .containsExactly("CORRECT_EVENT"); } -} \ No newline at end of file + @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 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"); + } +}