From 8ef6e5ef8fc15e805cd347101ddbb61788e0bbfb Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 17:46:09 +0200 Subject: [PATCH] Resolve path bindings through single-arg string helper methods. Enum.valueOf(normalize(event)) now expands when normalize trivially forwards its parameter; add expander regression test and assert flow highlight helper in enterprise HTML export. Co-authored-by: Cursor --- .../service/EntryPointBindingExpander.java | 81 +++++++++++++++++-- .../EntryPointBindingExpanderTest.java | 47 +++++++++++ .../EnterpriseDedicatedEndpointHtmlTest.java | 1 + 3 files changed, 122 insertions(+), 7 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java index 3240c7f..ad15b19 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java @@ -506,7 +506,7 @@ public final class EntryPointBindingExpander { return super.visit(node); } Expression argument = (Expression) node.arguments().get(0); - if (!argumentReferencesParameter(argument, paramName)) { + if (!argumentReferencesParameter(argument, paramName, method, context)) { return super.visit(node); } if (!isValueOfSiteCompatibleWithBindings(node, partialBindings)) { @@ -551,24 +551,91 @@ public final class EntryPointBindingExpander { } private static boolean argumentReferencesParameter(Expression argument, String paramName) { + return argumentReferencesParameter(argument, paramName, null, null); + } + + private static boolean argumentReferencesParameter( + Expression argument, + String paramName, + MethodDeclaration ownerMethod, + CodebaseContext context) { if (argument instanceof SimpleName simpleName) { return paramName.equals(simpleName.getIdentifier()); } if (argument instanceof MethodInvocation invocation - && "toUpperCase".equals(invocation.getName().getIdentifier()) && invocation.arguments().isEmpty() && invocation.getExpression() instanceof SimpleName simpleName) { - return paramName.equals(simpleName.getIdentifier()); + String methodName = invocation.getName().getIdentifier(); + if (("toUpperCase".equals(methodName) || "toLowerCase".equals(methodName) || "trim".equals(methodName)) + && paramName.equals(simpleName.getIdentifier())) { + return true; + } } if (argument instanceof MethodInvocation invocation - && "toLowerCase".equals(invocation.getName().getIdentifier()) - && invocation.arguments().isEmpty() - && invocation.getExpression() instanceof SimpleName simpleName) { - return paramName.equals(simpleName.getIdentifier()); + && ownerMethod != null + && context != null + && isSingleArgHelperForwardingParameter(invocation, paramName, ownerMethod, context)) { + return true; } return false; } + private static boolean isSingleArgHelperForwardingParameter( + MethodInvocation helperInvocation, + String paramName, + MethodDeclaration ownerMethod, + CodebaseContext context) { + if (helperInvocation.arguments().size() != 1) { + return false; + } + Expression forwarded = (Expression) helperInvocation.arguments().get(0); + if (!argumentReferencesParameter(forwarded, paramName, ownerMethod, context)) { + return false; + } + MethodDeclaration helper = findHelperMethod(ownerMethod, helperInvocation, context); + return helper != null && isTrivialSingleArgStringHelper(helper); + } + + private static MethodDeclaration findHelperMethod( + MethodDeclaration ownerMethod, + MethodInvocation helperInvocation, + CodebaseContext context) { + ASTNode parent = ownerMethod.getParent(); + if (!(parent instanceof TypeDeclaration typeDeclaration)) { + return null; + } + String helperName = helperInvocation.getName().getIdentifier(); + return context.findMethodDeclaration(typeDeclaration, helperName, true); + } + + private static boolean isTrivialSingleArgStringHelper(MethodDeclaration helper) { + if (helper.parameters().size() != 1 || helper.getBody() == null) { + return false; + } + String helperParam = null; + Object paramObj = helper.parameters().get(0); + if (paramObj instanceof SingleVariableDeclaration param) { + helperParam = param.getName().getIdentifier(); + } + if (helperParam == null) { + return false; + } + Set known = new LinkedHashSet<>(); + known.add(helperParam); + known.addAll(collectIntraMethodParameterAliases(helper, helperParam)); + final boolean[] returnsKnownBinding = {false}; + helper.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + if (expressionReferencesKnownBinding(node.getExpression(), known)) { + returnsKnownBinding[0] = true; + } + return super.visit(node); + } + }); + return returnsKnownBinding[0]; + } + private static String resolveValueOfEnumType(MethodInvocation valueOfInvocation, CodebaseContext context) { Expression receiver = valueOfInvocation.getExpression(); if (receiver != null) { diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpanderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpanderTest.java index 450c2d8..146924b 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpanderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpanderTest.java @@ -532,4 +532,51 @@ class EntryPointBindingExpanderTest { assertThat(variants).extracting(map -> map.get("event")) .containsExactlyInAnyOrder("PAY", "SHIP"); } + + @Test + void shouldExpandEventWhenParameterIsPassedThroughSingleArgHelper(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + public class OrderController { + OrderGateway gateway; + public void transition(String event) { + gateway.trigger(event); + } + } + class OrderGateway { + void trigger(String event) { + OrderEvent.valueOf(normalize(event)); + } + String normalize(String raw) { + return raw.toUpperCase(); + } + } + enum OrderEvent { PAY, SHIP } + """; + Files.writeString(tempDir.resolve("App.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + Map> graph = engine.buildCallGraph(); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("transition") + .name("POST /api/order/{event}") + .parameters(List.of(EntryPoint.Parameter.builder() + .name("event") + .type("String") + .annotations(List.of("PathVariable")) + .build())) + .build(); + + List> variants = + EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph); + + assertThat(variants).extracting(map -> map.get("event")) + .containsExactlyInAnyOrder("PAY", "SHIP"); + } } diff --git a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/EnterpriseDedicatedEndpointHtmlTest.java b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/EnterpriseDedicatedEndpointHtmlTest.java index ee11db2..88b1d59 100644 --- a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/EnterpriseDedicatedEndpointHtmlTest.java +++ b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/EnterpriseDedicatedEndpointHtmlTest.java @@ -45,6 +45,7 @@ class EnterpriseDedicatedEndpointHtmlTest { assertThat(html).contains("function highlightFlow(flow)"); assertThat(html).contains("\"source\" : \"click.kamil.enterprise.machines.order.OrderState.NEW\""); assertThat(html).contains("\"source\" : \"click.kamil.enterprise.machines.order.OrderState.PENDING\""); + assertThat(html).contains("function highlightEvents(steps)"); } private static Path findOrderMachineHtml(Path outputDir) throws Exception {