From 6596303b7d29b8fd109d0f376ad112fc78a75008 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sun, 21 Jun 2026 18:20:30 +0200 Subject: [PATCH] Replace fallback string scraper with precise AST traversal --- .../service/AbstractCallGraphEngine.java | 65 ++++++++++--------- .../PolymorphicStateMachineConfiguration.json | 12 +++- .../StateMachineConfig.json | 14 ++-- 3 files changed, 51 insertions(+), 40 deletions(-) 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 6832121..9a620bd 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 @@ -307,8 +307,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } } } - // (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; @@ -318,36 +318,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } } - 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); - } + // As a fallback, attempt direct AST extraction (e.g. from MessageBuilder chains or inline constructor args) + if (!hasValidConstant && exprNode instanceof org.eclipse.jdt.core.dom.Expression) { + extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) exprNode, polymorphicEvents); } - // 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; - }); - } + // The aggressive regex scraper has been completely removed to avoid false positives. + // We rely on precise AST traversal instead. + List newPolyEvents = new ArrayList<>(); for (String pe : polymorphicEvents) { List resolved = resolveClassConstantReturns(pe, context, null); @@ -359,6 +337,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } polymorphicEvents = newPolyEvents; + // 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 (!resolvedValue.equals(event) || !polymorphicEvents.isEmpty()) { return TriggerPoint.builder() .event(resolvedValue) @@ -631,7 +615,26 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } } - // 2. Delegate to method return analysis + // 2. Inline Instantiation Getter (e.g. new Event(CONSTANT).getType()) + if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { + for (Object argObj : cic.arguments()) { + extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) argObj, constants); + } + } + + // 3. Builder Pattern (e.g. MessageBuilder.withPayload(...).setHeader("event", CONSTANT).build()) + org.eclipse.jdt.core.dom.Expression current = mi; + while (current instanceof org.eclipse.jdt.core.dom.MethodInvocation chainMi) { + String chainName = chainMi.getName().getIdentifier(); + if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) { + extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(1), constants); + } else if (chainName.equals("event") && chainMi.arguments().size() == 1) { + extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) chainMi.arguments().get(0), constants); + } + current = chainMi.getExpression(); + } + + // 4. Delegate to method return analysis TypeDeclaration td = findEnclosingType(mi); if (td != null && (mi.getExpression() == null || mi.getExpression() instanceof org.eclipse.jdt.core.dom.ThisExpression)) { List values = resolveMethodReturnConstant(context.getFqn(td), methodName, 0, new java.util.HashSet<>(), null); diff --git a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json index b85df40..20d01b6 100644 --- a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json +++ b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json @@ -381,10 +381,18 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 13, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvents.PAY", "OrderEvents.CANCEL" ] }, "contextMachineId" : null, - "matchedTransitions" : null + "matchedTransitions" : [ { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.PAID", + "event" : "OrderEvents.PAY" + }, { + "sourceState" : "OrderStates.SUBMITTED", + "targetState" : "OrderStates.CANCELED", + "event" : "OrderEvents.CANCEL" + } ] }, { "entryPoint" : { "type" : "REST", diff --git a/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json b/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json index 75d0842..1e0151b 100644 --- a/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json +++ b/state_machine_exporter/src/test/resources/golden/StateMachineConfig/StateMachineConfig.json @@ -144,7 +144,7 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 25, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvent.PROCESS" ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -175,7 +175,7 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 25, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvent.COMPLETE" ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -206,7 +206,7 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 25, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvent.CANCEL" ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -241,7 +241,7 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 25, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvent.COMPLETE" ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -276,7 +276,7 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 78, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvent.PROCESS" ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -311,7 +311,7 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 25, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvent.PROCESS" ] }, "contextMachineId" : null, "matchedTransitions" : [ { @@ -346,7 +346,7 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 50, - "polymorphicEvents" : [ ] + "polymorphicEvents" : [ "OrderEvent.CANCEL" ] }, "contextMachineId" : null, "matchedTransitions" : [ {