From 60cd6511d4827d0c8bad5521945f7496a4e9a006 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 18:43:12 +0200 Subject: [PATCH] Expand path bindings from static initializer map put entries. When a static map field is populated in a static block via put calls with compile-time string keys, expand endpoint path parameters through those entries while remaining fail-closed for computed keys. Co-authored-by: Cursor --- .../service/EntryPointBindingExpander.java | 74 ++++++++++++++++++- .../EntryPointBindingExpanderTest.java | 59 ++++++++++++++- 2 files changed, 129 insertions(+), 4 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 f19f9a7..a26e24a 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 @@ -688,16 +688,84 @@ public final class EntryPointBindingExpander { if (fragmentObj instanceof VariableDeclarationFragment fragment && fieldName.equals(fragment.getName().getIdentifier())) { Expression initializer = fragment.getInitializer(); - if (initializer == null) { - return null; + if (initializer != null) { + Map direct = extractMapOfLiteralEntries(initializer, typeDecl, context); + if (direct != null) { + return direct; + } } - return extractMapOfLiteralEntries(initializer, typeDecl, context); + return extractStaticBlockMapPutEntries(typeDecl, fieldName, context); } } } return null; } + private static Map extractStaticBlockMapPutEntries( + TypeDeclaration typeDecl, + String fieldName, + CodebaseContext context) { + LinkedHashMap entries = new LinkedHashMap<>(); + boolean[] foundPut = {false}; + boolean[] invalid = {false}; + for (Object bodyDeclObj : typeDecl.bodyDeclarations()) { + if (!(bodyDeclObj instanceof Initializer initializer) || !isStaticInitializer(initializer)) { + continue; + } + Block block = initializer.getBody(); + if (block == null) { + continue; + } + block.accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + if (invalid[0] || !"put".equals(node.getName().getIdentifier()) + || node.arguments().size() != 2) { + return super.visit(node); + } + if (!mapReceiverReferencesField(node.getExpression(), fieldName)) { + return super.visit(node); + } + String key = resolveCompileTimeStringKey( + (Expression) node.arguments().get(0), typeDecl, context); + if (key == null) { + invalid[0] = true; + return super.visit(node); + } + entries.put(key, "*"); + foundPut[0] = true; + return super.visit(node); + } + }); + } + if (invalid[0] || !foundPut[0] || entries.isEmpty()) { + return null; + } + return entries; + } + + private static boolean isStaticInitializer(Initializer initializer) { + for (Object modifierObj : initializer.modifiers()) { + if (modifierObj instanceof Modifier modifier && modifier.isStatic()) { + return true; + } + } + return false; + } + + private static boolean mapReceiverReferencesField(Expression receiver, String fieldName) { + if (receiver instanceof SimpleName simpleName) { + return fieldName.equals(simpleName.getIdentifier()); + } + if (receiver instanceof FieldAccess fieldAccess) { + return fieldName.equals(fieldAccess.getName().getIdentifier()); + } + if (receiver instanceof QualifiedName qualifiedName) { + return fieldName.equals(qualifiedName.getName().getIdentifier()); + } + return false; + } + private static Map extractMapOfLiteralEntries( Expression expression, TypeDeclaration declaringType, 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 bb3b701..4c40d32 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 @@ -2994,7 +2994,7 @@ class EntryPointBindingExpanderTest { } @Test - void shouldNotExpandCommandKeyFromStaticInitializerMap(@TempDir Path tempDir) throws Exception { + void shouldExpandCommandKeyFromStaticInitializerMapPutEntries(@TempDir Path tempDir) throws Exception { String source = """ package com.example; import java.util.HashMap; @@ -3029,6 +3029,63 @@ class EntryPointBindingExpanderTest { HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); Map> graph = engine.buildCallGraph(); + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.GenericCommandController") + .methodName("execute") + .name("POST /api/commands/{commandKey}") + .parameters(List.of(EntryPoint.Parameter.builder() + .name("commandKey") + .type("String") + .annotations(List.of("PathVariable")) + .build())) + .build(); + + List> variants = + EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph); + + assertThat(variants).extracting(map -> map.get("commandKey")) + .containsExactlyInAnyOrder("order.pay", "order.ship"); + } + + @Test + void shouldNotExpandCommandKeyFromStaticInitializerMapWithComputedPutKeys(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + import java.util.HashMap; + import java.util.Map; + public class GenericCommandController { + OrderGateway gateway; + public void execute(String commandKey) { + gateway.trigger(commandKey); + } + } + class OrderGateway { + private static final Map ROUTES; + static { + ROUTES = new HashMap<>(); + ROUTES.put(prefix("order.pay"), OrderEvent.PAY); + ROUTES.put("order.ship", OrderEvent.SHIP); + } + static String prefix(String value) { + return value; + } + void trigger(String commandKey) { + StateMachine sm = new StateMachine(); + sm.sendEvent(ROUTES.get(commandKey)); + } + } + class StateMachine { void sendEvent(OrderEvent event) {} } + 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.GenericCommandController") .methodName("execute")