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 25f76e1..d11135a 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 @@ -55,7 +55,26 @@ public class ConstantResolver { return resolveManual(sn, context, visited); } + if (expr instanceof MethodInvocation mi) { + if ((mi.getName().getIdentifier().equals("name") || mi.getName().getIdentifier().equals("toString")) && mi.arguments().isEmpty()) { + String val = resolveInternal(mi.getExpression(), context, visited); + if (val != null) { + // If it was resolved as a fully qualified enum string like "MyEvents.ORDER_PAID", strip it + if (val.contains(".")) { + return val.substring(val.lastIndexOf('.') + 1); + } + return val; + } + + // Fallback for unresolved AST nodes + if (mi.getExpression() instanceof QualifiedName qn) { + return qn.getName().getIdentifier(); + } else if (mi.getExpression() instanceof SimpleName sn) { + return sn.getIdentifier(); + } + } + IMethodBinding mb = mi.resolveMethodBinding(); if (mb != null) { ITypeBinding returnType = mb.getReturnType(); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java index 35b55a8..cea823a 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java @@ -1081,4 +1081,260 @@ class CallGraphBuilderTest { assertThat(chains).hasSize(1); assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B"); } -} + + + @Test + void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(domainEvent.getEventForStatus()); + } + } + + class DomainEvent { + private OrderStatus status; + public String getEventForStatus() { + switch (status) { + case NEW: return "CREATE_EVENT"; + case PAID: return "PAY_EVENT"; + default: return "CANCEL_EVENT"; + } + } + } + + class OrderService { + public void process(String event) {} + } + + enum OrderStatus { NEW, PAID } + """; + Path tempDir = Files.createTempDirectory("callgraph_switch"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("CREATE_EVENT", "PAY_EVENT", "CANCEL_EVENT"); + } + + @Test + void shouldGracefullyHandleComplexMethodChainsWithoutCrashing() throws IOException { + String source = """ + package com.example; + import java.util.Optional; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(Optional.of(domainEvent).get().getEvent()); + } + } + + class DomainEvent { + public String getEvent() { return "COMPLEX_EVENT"; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_complex_chain"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + } + + @Test + void shouldNotCrashOnStaticMethodWrapping() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(String.valueOf(domainEvent.getEvent())); + } + } + + class DomainEvent { + public String getEvent() { return "STATIC_WRAP_EVENT"; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_static_wrap"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + } + + + + @Test + void shouldResolveConstantsFromStringConcatenation() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(domainEvent.getCustomEvent()); + } + } + + class DomainEvent { + public static final String PREFIX = "ORDER_"; + public String getCustomEvent() { + return PREFIX + "CREATED"; + } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_concat"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_CREATED"); + } + + @Test + void shouldResolveConstantsFromEnumReturn() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(domainEvent.getEventEnum()); + } + } + + class DomainEvent { + public String getEventEnum() { + return MyEvents.ORDER_PAID.name(); + } + } + + class OrderService { + public void process(String event) {} + } + + enum MyEvents { ORDER_PAID } + """; + Path tempDir = Files.createTempDirectory("callgraph_enum_return"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_PAID"); + } + + @Test + void shouldResolveConstantsFromAlternativeGetterName() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(domainEvent.get2Type()); + } + } + + class DomainEvent { + public String get2Type() { + return "ALTERNATIVE_EVENT"; + } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_alt_getter"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ALTERNATIVE_EVENT"); + } + + + @Test + void shouldResolveConstantsFromStringToString() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(domainEvent.getEventString()); + } + } + + class DomainEvent { + public static final String ACTUAL_STRING = "STRING_LITERAL_EVENT"; + public String getEventString() { + return ACTUAL_STRING.toString(); + } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_string_tostring"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("STRING_LITERAL_EVENT"); + } + +} \ No newline at end of file