diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java index 5fdc117..9f99830 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java @@ -222,6 +222,50 @@ public class CallGraphBuilder { } } + // LAST RESORT FALLBACK: If AST deep trace failed to find a valid ALL_CAPS constant + // (e.g. it returned a field name like 'type', or nothing), we can scrape the resolvedValue string directly + // for string literals or enum-like constants. + boolean hasValidConstant = false; + for (String ev : polymorphicEvents) { + String val = ev.contains(".") ? ev.substring(ev.lastIndexOf('.') + 1) : ev; + if (val.equals(val.toUpperCase()) && val.length() > 2) { + hasValidConstant = true; + break; + } + } + + if (!hasValidConstant && resolvedValue.contains("(")) { + java.util.List scraped = new java.util.ArrayList<>(); + + // Extract "STRING_LITERALS" + java.util.regex.Matcher m1 = java.util.regex.Pattern.compile("\"([^\"]+)\"").matcher(resolvedValue); + while (m1.find()) { + scraped.add(m1.group(1)); + } + + // Extract ENUM_LIKE_CONSTANTS + java.util.regex.Matcher m2 = java.util.regex.Pattern.compile("\\b([A-Z_]{3,})\\b").matcher(resolvedValue); + while (m2.find()) { + if (!scraped.contains(m2.group(1))) { + scraped.add(m2.group(1)); + } + } + + if (!scraped.isEmpty()) { + polymorphicEvents.clear(); + polymorphicEvents.addAll(scraped); + } + } + + // Clean up any remaining invalid AST fallbacks (like 'type', 'event') if we found valid ones + if (polymorphicEvents.size() > 1) { + polymorphicEvents.removeIf(e -> { + String val = e.contains(".") ? e.substring(e.lastIndexOf('.') + 1) : e; + return !val.equals(val.toUpperCase()) || val.length() <= 2; + }); + } + + if (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) { return TriggerPoint.builder() .event(resolvedValue) 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 36ecf19..6e651af 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 @@ -1856,4 +1856,45 @@ class CallGraphBuilderTest { org.assertj.core.api.Assertions.assertThat(allEvents).containsExactlyInAnyOrder("FIELD_DECL", "INIT_BLOCK"); } + + @Test + void shouldFallbackToScraperForComplexInlineInstantiations() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus() { + // The AST tracer will trace EventWithConstructorArg.getType() + // It will find `this.type = typeArg`, which it cannot resolve because it's a parameter. + // The fallback scraper should catch "COMPLEX_INLINE_CONST". + service.process(new EventWithConstructorArg("COMPLEX_INLINE_CONST", 123).getType()); + } + } + + class EventWithConstructorArg { + private String type; + public EventWithConstructorArg(String typeArg, int other) { + this.type = typeArg; + } + public String getType() { return type; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_scraper"); + 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()).contains("COMPLEX_INLINE_CONST"); + } + } \ No newline at end of file