From 0cb78e5dfd856a315949785a0a568f39956c07da Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sun, 5 Jul 2026 15:57:53 +0200 Subject: [PATCH] update number 5 --- .../analysis/model/TriggerPoint.java | 3 + .../analysis/resolver/ConstantResolver.java | 12 +- .../service/AbstractCallGraphEngine.java | 103 ++++++++++++---- .../analysis/service/ConstantExtractor.java | 11 +- .../service/HeuristicCallGraphEngine.java | 39 ------ .../analysis/service/JdtCallGraphEngine.java | 38 ------ .../ast/common/CodebaseContext.java | 10 +- ...uristicCallGraphEnginePolymorphicTest.java | 116 ++++++++++++++++++ 8 files changed, 214 insertions(+), 118 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java index 404ef37..4aa6d58 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java @@ -53,6 +53,9 @@ public class TriggerPoint { this.eventTypeFqn = eventTypeFqn; this.event = qualifyEvent(event, eventTypeFqn); if (polymorphicEvents != null) { + if (polymorphicEvents.contains("CustomEvents.ALPHA")) { + new Exception("DEBUG TriggerPoint constructed with CustomEvents.ALPHA!").printStackTrace(System.out); + } this.polymorphicEvents = polymorphicEvents.stream() .map(pe -> qualifyEvent(pe, eventTypeFqn)) .collect(java.util.stream.Collectors.toList()); 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 dd566f3..fd0d419 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 @@ -132,13 +132,7 @@ public class ConstantResolver { return null; } } - ITypeBinding returnType = mb.getReturnType(); - if (returnType != null) { - java.util.List values = context.getEnumValues(returnType.getQualifiedName()); - if (values != null && !values.isEmpty()) { - return "ENUM_SET:" + String.join(",", values); - } - } + } else { TypeDeclaration td = null; if (mi.getExpression() != null) { @@ -171,8 +165,7 @@ public class ConstantResolver { if (md != null) { String evaluated = evaluateMethodOutput(mi, md, context, visited); if (evaluated != null) return evaluated; - - if (md.getReturnType2() != null) { + if (mi.getName().getIdentifier().startsWith("get") && md.getReturnType2() != null) { java.util.List values = context.getEnumValues(md.getReturnType2().toString()); if (values != null && !values.isEmpty()) { return "ENUM_SET:" + String.join(",", values); @@ -182,7 +175,6 @@ public class ConstantResolver { } } } - return null; } 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 023157e..5578cc7 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 @@ -112,6 +112,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(path); try { String event = tp.getEvent(); + event = unwrapConverterMethods(event); String currentParamName = event; String resolvedValue = event; String methodSuffix = ""; @@ -194,30 +195,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { if (paramIndex < edge.getArguments().size()) { String arg = edge.getArguments().get(paramIndex); if (arg != null) { - if (arg.contains("(") && !arg.startsWith("new ")) { - org.eclipse.jdt.core.dom.ASTParser argParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17); - argParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION); - argParser.setSource(arg.toCharArray()); - org.eclipse.jdt.core.dom.ASTNode argNode = argParser.createAST(null); - if (argNode instanceof org.eclipse.jdt.core.dom.MethodInvocation miArg && !miArg.arguments().isEmpty()) { - boolean shouldUnpack = false; - org.eclipse.jdt.core.dom.Expression recv = miArg.getExpression(); - if (recv == null || recv instanceof org.eclipse.jdt.core.dom.ThisExpression) { - shouldUnpack = true; - } else if (recv instanceof org.eclipse.jdt.core.dom.SimpleName sn) { - shouldUnpack = !Character.isLowerCase(sn.getIdentifier().charAt(0)); - } else if (recv instanceof org.eclipse.jdt.core.dom.QualifiedName qn) { - shouldUnpack = !Character.isLowerCase(qn.getName().getIdentifier().charAt(0)); - } - if (shouldUnpack) { - org.eclipse.jdt.core.dom.Expression innerArg = (org.eclipse.jdt.core.dom.Expression) miArg.arguments().get(0); - if (miArg.getName().getIdentifier().equals("valueOf") && miArg.arguments().size() == 2) { - innerArg = (org.eclipse.jdt.core.dom.Expression) miArg.arguments().get(1); - } - arg = innerArg.toString(); - } - } - } + arg = unwrapConverterMethods(arg); String receiver = edge.getReceiver(); String actualReceiver = null; if ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super.")) { @@ -270,6 +248,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents); } if (!setterEvents.isEmpty()) { + System.out.println("DEBUG Early return 1: setterEvents = " + setterEvents); return TriggerPoint.builder() .event(tp.getEvent()) .className(tp.getClassName()) @@ -289,11 +268,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } } - boolean hasGetterOnReceiver = methodSuffix.startsWith(".get") && currentParamName != null + boolean hasGetterOnReceiver = (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) && currentParamName != null && (currentParamName.contains("new ") || (!currentParamName.replaceFirst("^this\\.", "").contains(".") && !currentParamName.contains("("))); if (hasGetterOnReceiver) { List getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph); if (getterEvents != null && !getterEvents.isEmpty()) { + System.out.println("DEBUG Early return 2: getterEvents = " + getterEvents); return TriggerPoint.builder() .event(tp.getEvent()) .className(tp.getClassName()) @@ -323,6 +303,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } } + System.out.println("DEBUG resolvedValue before ENUM_SET check: " + resolvedValue + " for path: " + path); List polymorphicEvents = new ArrayList<>(); if (resolvedValue.startsWith("ENUM_SET:")) { @@ -594,6 +575,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } } + System.out.println("DEBUG declaredType before getEnumValues: " + declaredType + " for exprNode: " + exprNode + " in " + entryMethod); if (declaredType != null) { List enumValues = context.getEnumValues(declaredType); if (enumValues != null && !enumValues.isEmpty()) { @@ -722,7 +704,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList()); - System.out.println("JSON DEBUG: building final TriggerPoint. resolvedValue=" + resolvedValue + ", event=" + event); if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) { return TriggerPoint.builder() .event(resolvedValue) @@ -1414,4 +1395,74 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { protected abstract Map> buildCallGraph(); protected abstract String resolveCalledMethod(MethodInvocation node); + + protected List resolveCalledMethodsPolymorphic(MethodInvocation node) { + String baseCalled = resolveCalledMethod(node); + if (baseCalled == null) return Collections.emptyList(); + + List allResolved = new ArrayList<>(); + + if (node.getExpression() == null) { + return Collections.singletonList(baseCalled); + } + + if (baseCalled.contains(".")) { + String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); + String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); + + List impls = context.getImplementations(className); + if (impls.isEmpty()) { + allResolved.add(baseCalled); + } else { + TypeDeclaration td = context.getTypeDeclaration(className); + boolean isAbstractOrInterface = false; + if (td != null) { + isAbstractOrInterface = td.isInterface() || (td.modifiers() != null && td.modifiers().toString().contains("abstract")); + } + boolean isImplicitThis = node.getExpression() == null || node.getExpression() instanceof ThisExpression; + if (!isAbstractOrInterface || isImplicitThis) { + allResolved.add(baseCalled); + } + for (String impl : impls) { + allResolved.add(impl + "." + methodName); + } + } + } else { + allResolved.add(baseCalled); + } + + return allResolved; + } + + private String unwrapConverterMethods(String arg) { + if (arg == null) return null; + String current = arg; + while (current.contains("(") && !current.startsWith("new ")) { + org.eclipse.jdt.core.dom.ASTParser argParser = org.eclipse.jdt.core.dom.ASTParser.newParser(org.eclipse.jdt.core.dom.AST.JLS17); + argParser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_EXPRESSION); + argParser.setSource(current.toCharArray()); + org.eclipse.jdt.core.dom.ASTNode argNode = argParser.createAST(null); + if (argNode instanceof org.eclipse.jdt.core.dom.MethodInvocation miArg && !miArg.arguments().isEmpty()) { + boolean shouldUnpack = false; + org.eclipse.jdt.core.dom.Expression recv = miArg.getExpression(); + if (recv == null || recv instanceof org.eclipse.jdt.core.dom.ThisExpression) { + shouldUnpack = true; + } else if (recv instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + shouldUnpack = !Character.isLowerCase(sn.getIdentifier().charAt(0)); + } else if (recv instanceof org.eclipse.jdt.core.dom.QualifiedName qn) { + shouldUnpack = !Character.isLowerCase(qn.getName().getIdentifier().charAt(0)); + } + if (shouldUnpack) { + org.eclipse.jdt.core.dom.Expression innerArg = (org.eclipse.jdt.core.dom.Expression) miArg.arguments().get(0); + if (miArg.getName().getIdentifier().equals("valueOf") && miArg.arguments().size() == 2) { + innerArg = (org.eclipse.jdt.core.dom.Expression) miArg.arguments().get(1); + } + current = innerArg.toString(); + continue; + } + } + break; + } + return current; + } } \ No newline at end of file diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java index 2891adf..e8e5a33 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/ConstantExtractor.java @@ -33,7 +33,14 @@ public class ConstantExtractor { if (constantResolver != null) { String resolved = constantResolver.resolve(expr, context); if (resolved != null) { - constants.add(resolved); + if (resolved.startsWith("ENUM_SET:")) { + for (String eVal : resolved.substring(9).split(",")) { + String parsed = parseEnumSetElement(eVal); + if (!constants.contains(parsed)) constants.add(parsed); + } + } else { + constants.add(resolved); + } return; } } @@ -274,6 +281,7 @@ public class ConstantExtractor { } public List resolveMethodReturnConstant(String className, String methodName, int depth, Set visited, CompilationUnit contextCu) { + System.out.println("DEBUG ConstantExtractor.resolveMethodReturnConstant CALLED: className=" + className + ", methodName=" + methodName); log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth); if (depth > 20) { log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName); @@ -384,6 +392,7 @@ public class ConstantExtractor { } } visited.remove(fqn); + System.out.println("DEBUG resolveMethodReturnConstant className=" + className + " methodName=" + methodName + " returns: " + constants); log.debug("resolveMethodReturnConstant for class={}, method={} resolved constants: {}", className, methodName, constants); return constants; } 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 531721e..5f0eb1d 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 @@ -160,45 +160,6 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine { return resolvedValue; } - - protected List resolveCalledMethodsPolymorphic(MethodInvocation node) { - String baseCalled = resolveCalledMethod(node); - if (baseCalled == null) return Collections.emptyList(); - - List allResolved = new ArrayList<>(); - - if (node.getExpression() == null) { - return Collections.singletonList(baseCalled); - } - - if (baseCalled.contains(".")) { - String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); - String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); - - List impls = context.getImplementations(className); - if (impls.isEmpty()) { - allResolved.add(className + "." + methodName); - } else { - TypeDeclaration td = context.getTypeDeclaration(className); - boolean isAbstractOrInterface = false; - if (td != null) { - isAbstractOrInterface = td.isInterface() || (td.modifiers() != null && td.modifiers().toString().contains("abstract")); - } - boolean isImplicitThis = node.getExpression() == null || node.getExpression() instanceof ThisExpression || node.getExpression() instanceof SuperMethodInvocation; - if (!isAbstractOrInterface || isImplicitThis) { - allResolved.add(className + "." + methodName); - } - for (String impl : impls) { - allResolved.add(impl + "." + methodName); - } - } - } else { - allResolved.add(baseCalled); - } - - return allResolved; - } - private String findDefiningClass(String className, String methodName) { TypeDeclaration td = context.getTypeDeclaration(className); if (td == null) return null; 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 cec5d49..943117e 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 @@ -158,44 +158,6 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { return super.resolveArgument(expr); } - private List resolveCalledMethodsPolymorphic(MethodInvocation node) { - String baseCalled = resolveCalledMethod(node); - if (baseCalled == null) return Collections.emptyList(); - - List allResolved = new ArrayList<>(); - - if (node.getExpression() == null) { - return Collections.singletonList(baseCalled); - } - - if (baseCalled.contains(".")) { - String className = baseCalled.substring(0, baseCalled.lastIndexOf('.')); - String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1); - - List impls = context.getImplementations(className); - if (impls.isEmpty()) { - allResolved.add(baseCalled); - } else { - TypeDeclaration td = context.getTypeDeclaration(className); - boolean isAbstractOrInterface = false; - if (td != null) { - isAbstractOrInterface = td.isInterface() || (td.modifiers() != null && td.modifiers().toString().contains("abstract")); - } - boolean isImplicitThis = node.getExpression() == null || node.getExpression() instanceof ThisExpression; - if (!isAbstractOrInterface || isImplicitThis) { - allResolved.add(baseCalled); - } - for (String impl : impls) { - allResolved.add(impl + "." + methodName); - } - } - } else { - allResolved.add(baseCalled); - } - - return allResolved; - } - protected String resolveCalledMethod(MethodInvocation node) { Expression receiver = node.getExpression(); String methodName = node.getName().getIdentifier(); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index 1fcbc01..917bbb7 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -374,10 +374,12 @@ public class CodebaseContext { cleanName = cleanName.trim(); List values = enumValues.get(cleanName); - if (values != null) return values; - String fqn = simpleNameToFqn.get(cleanName); - if (fqn != null) return enumValues.get(fqn); - return null; + if (values == null) { + String fqn = simpleNameToFqn.get(cleanName); + if (fqn != null) values = enumValues.get(fqn); + } + System.out.println("DEBUG getEnumValues called for: " + fqnOrSimpleName + " resolved to " + cleanName + ". Returns: " + values); + return values; } public State resolveState(Expression expr, CompilationUnit cu) { diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEnginePolymorphicTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEnginePolymorphicTest.java index 5c2466f..43feb4f 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEnginePolymorphicTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEnginePolymorphicTest.java @@ -377,6 +377,122 @@ class HeuristicCallGraphEnginePolymorphicTest { .containsExactlyInAnyOrder("CustomEvents.ALPHA", "CustomEvents.BETA"); } + @Test + void shouldResolvePolymorphicEventsThroughNestedConverters() throws IOException { + String source = """ + package com.example; + public class Dispatcher { + private HandlerService service; + public void dispatchRequest(Payload domainEvent) { + doDispatch(domainEvent); + } + + private void doDispatch(Payload domainEvent) { + // NESTED CONVERTERS + service.handleEvent(verify(sanitize(domainEvent.type()))); + } + + private CustomEvents sanitize(CustomEvents e) { return e; } + private CustomEvents verify(CustomEvents e) { return e; } + } + + class HandlerService { + public void handleEvent(CustomEvents event) {} + } + + interface Payload { + CustomEvents type(); + } + + class AlphaPayload implements Payload { + public CustomEvents type() { return CustomEvents.ALPHA; } + } + + enum CustomEvents { ALPHA, BETA } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_poly_nested"); + Files.writeString(tempDir.resolve("DispatcherConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.Dispatcher") + .methodName("dispatchRequest") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.HandlerService") + .methodName("handleEvent") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + System.out.println("DEBUG context.getImplementations(Payload) = " + context.getImplementations("Payload")); + System.out.println("DEBUG context.getImplementations(com.example.Payload) = " + context.getImplementations("com.example.Payload")); + System.out.println("DEBUG getPolymorphicEvents() = " + chains.get(0).getTriggerPoint().getPolymorphicEvents()); + assertThat(chains).hasSize(1); + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) + .containsExactly("CustomEvents.ALPHA"); + } + + @Test + void shouldResolvePolymorphicEventsWithCustomGetterSuffixes() throws IOException { + String source = """ + package com.example; + public class Dispatcher { + private HandlerService service; + public void dispatchRequest(Payload domainEvent) { + doDispatch(domainEvent); + } + + private void doDispatch(Payload domainEvent) { + // Using .event() instead of .getEvent() + service.handleEvent(check(domainEvent.event())); + } + + private CustomEvents check(CustomEvents e) { return e; } + } + + class HandlerService { + public void handleEvent(CustomEvents event) {} + } + + interface Payload { + CustomEvents event(); + } + + class BetaPayload implements Payload { + public CustomEvents event() { return CustomEvents.BETA; } + } + + enum CustomEvents { ALPHA, BETA } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_poly_suffix"); + Files.writeString(tempDir.resolve("DispatcherConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.Dispatcher") + .methodName("dispatchRequest") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.HandlerService") + .methodName("handleEvent") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + assertThat(chains).hasSize(1); + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()) + .containsExactly("CustomEvents.BETA"); + } + @Test void shouldResolveMappedTransitionEnumThroughAbstractBaseClassAndMultipleArgs() throws IOException { String source = """