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 0365560..023157e 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 @@ -194,6 +194,30 @@ 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(); + } + } + } String receiver = edge.getReceiver(); String actualReceiver = null; if ("this".equals(arg) || arg.startsWith("this.") || "super".equals(arg) || arg.startsWith("super.")) { 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 0957553..cec5d49 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 @@ -164,6 +164,10 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { 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); @@ -177,7 +181,8 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine { if (td != null) { isAbstractOrInterface = td.isInterface() || (td.modifiers() != null && td.modifiers().toString().contains("abstract")); } - if (!isAbstractOrInterface) { + boolean isImplicitThis = node.getExpression() == null || node.getExpression() instanceof ThisExpression; + if (!isAbstractOrInterface || isImplicitThis) { allResolved.add(baseCalled); } for (String impl : impls) { 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 660e08c..5c2466f 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 @@ -313,6 +313,70 @@ class HeuristicCallGraphEnginePolymorphicTest { org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("POST_ACCEPTANCE_REJECTED"); } + @Test + void shouldResolvePolymorphicEventsThroughAssertSupportedMethod() 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) { + service.handleEvent(checkAndMapEvent(domainEvent.getType())); + } + + private CustomEvents checkAndMapEvent(CustomEvents e) { + return e; + } + } + + class HandlerService { + public void handleEvent(CustomEvents event) {} + } + + interface Payload { + CustomEvents getType(); + } + + class AlphaPayload implements Payload { + public CustomEvents getType() { return CustomEvents.ALPHA; } + } + + class BetaPayload implements Payload { + public CustomEvents getType() { return CustomEvents.BETA; } + } + + enum CustomEvents { ALPHA, BETA } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_poly_assert"); + 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); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("CustomEvents.ALPHA", "CustomEvents.BETA"); + } + @Test void shouldResolveMappedTransitionEnumThroughAbstractBaseClassAndMultipleArgs() throws IOException { String source = """