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 a26e24a..84168a3 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 @@ -693,6 +693,10 @@ public final class EntryPointBindingExpander { if (direct != null) { return direct; } + Map doubleBrace = extractDoubleBraceMapPutEntries(initializer, typeDecl, context); + if (doubleBrace != null) { + return doubleBrace; + } } return extractStaticBlockMapPutEntries(typeDecl, fieldName, context); } @@ -744,6 +748,58 @@ public final class EntryPointBindingExpander { return entries; } + private static Map extractDoubleBraceMapPutEntries( + Expression initializer, + TypeDeclaration declaringType, + CodebaseContext context) { + if (!(initializer instanceof ClassInstanceCreation creation)) { + return null; + } + AnonymousClassDeclaration anonymous = creation.getAnonymousClassDeclaration(); + if (anonymous == null) { + return null; + } + LinkedHashMap entries = new LinkedHashMap<>(); + boolean[] foundPut = {false}; + boolean[] invalid = {false}; + for (Object bodyDeclObj : anonymous.bodyDeclarations()) { + if (!(bodyDeclObj instanceof Initializer instanceInitializer) + || isStaticInitializer(instanceInitializer)) { + continue; + } + Block block = instanceInitializer.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); + } + Expression receiver = node.getExpression(); + if (receiver != null && !(receiver instanceof ThisExpression)) { + return super.visit(node); + } + String key = resolveCompileTimeStringKey( + (Expression) node.arguments().get(0), declaringType, 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()) { 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 4c40d32..0154a7c 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 @@ -3102,4 +3102,110 @@ class EntryPointBindingExpanderTest { assertThat(variants).isEmpty(); } + + @Test + void shouldExpandCommandKeyFromDoubleBraceInitializerMapPutEntries(@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 = new HashMap<>() {{ + put("order.pay", OrderEvent.PAY); + put("order.ship", OrderEvent.SHIP); + }}; + 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") + .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 shouldNotExpandCommandKeyFromDoubleBraceInitializerMapWithComputedPutKeys(@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 = new HashMap<>() {{ + put(prefix("order.pay"), OrderEvent.PAY); + 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") + .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).isEmpty(); + } }