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 a6c1329..95a3f81 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 @@ -610,11 +610,31 @@ public final class EntryPointBindingExpander { Expression receiver, MethodDeclaration ownerMethod, CodebaseContext context) { + CompilationUnit compilationUnit = null; + TypeDeclaration defaultDeclaringType = null; + if (ownerMethod != null) { + if (ownerMethod.getRoot() instanceof CompilationUnit cu) { + compilationUnit = cu; + } + defaultDeclaringType = AstUtils.findEnclosingType(ownerMethod); + } + return resolveCompileTimeMapEntries( + receiver, defaultDeclaringType, compilationUnit, context); + } + + private static Map resolveCompileTimeMapEntries( + Expression receiver, + TypeDeclaration defaultDeclaringType, + CompilationUnit compilationUnit, + CodebaseContext context) { if (receiver == null) { return null; } + TypeDeclaration literalDeclaringType = defaultDeclaringType != null + ? defaultDeclaringType + : AstUtils.findEnclosingType(receiver); Map direct = extractMapOfLiteralEntries( - receiver, AstUtils.findEnclosingType(receiver), context); + receiver, literalDeclaringType, context); if (direct != null) { return direct; } @@ -632,7 +652,6 @@ public final class EntryPointBindingExpander { if (declaringType == null) { Expression qualifier = fieldAccess.getExpression(); if (qualifier instanceof SimpleName typeName) { - CompilationUnit compilationUnit = (CompilationUnit) ownerMethod.getRoot(); declaringType = context.getTypeDeclaration(typeName.getIdentifier(), compilationUnit); if (declaringType == null) { declaringType = context.getTypeDeclaration(typeName.getIdentifier()); @@ -651,11 +670,8 @@ public final class EntryPointBindingExpander { declaringType = context.getTypeDeclaration(declaringClass.getQualifiedName()); } } - if (declaringType == null && ownerMethod != null) { - ASTNode root = ownerMethod.getRoot(); - if (root instanceof CompilationUnit compilationUnit) { - declaringType = context.resolveStaticImport(fieldName, compilationUnit); - } + if (declaringType == null && compilationUnit != null) { + declaringType = context.resolveStaticImport(fieldName, compilationUnit); } if (declaringType == null) { declaringType = AstUtils.findEnclosingType(receiver); @@ -664,7 +680,6 @@ public final class EntryPointBindingExpander { fieldName = qualifiedName.getName().getIdentifier(); Name qualifier = qualifiedName.getQualifier(); if (qualifier instanceof SimpleName typeName) { - CompilationUnit compilationUnit = (CompilationUnit) ownerMethod.getRoot(); declaringType = context.getTypeDeclaration(typeName.getIdentifier(), compilationUnit); if (declaringType == null) { declaringType = context.getTypeDeclaration(typeName.getIdentifier()); @@ -771,21 +786,35 @@ public final class EntryPointBindingExpander { block.accept(new ASTVisitor() { @Override public boolean visit(MethodInvocation node) { - if (invalid[0] || !"put".equals(node.getName().getIdentifier()) - || node.arguments().size() != 2) { + if (invalid[0] || !mapReceiverReferencesAnyField(node.getExpression(), putReceivers)) { return super.visit(node); } - if (!mapReceiverReferencesAnyField(node.getExpression(), putReceivers)) { + String methodName = node.getName().getIdentifier(); + if ("put".equals(methodName) && node.arguments().size() == 2) { + 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); } - String key = resolveCompileTimeStringKey( - (Expression) node.arguments().get(0), typeDecl, context); - if (key == null) { - invalid[0] = true; + if ("putAll".equals(methodName) && node.arguments().size() == 1) { + CompilationUnit compilationUnit = block.getRoot() instanceof CompilationUnit cu + ? cu + : null; + Map merged = resolveCompileTimeMapEntries( + (Expression) node.arguments().get(0), typeDecl, compilationUnit, context); + if (merged == null || merged.isEmpty()) { + invalid[0] = true; + return super.visit(node); + } + entries.putAll(merged); + foundPut[0] = true; return super.visit(node); } - entries.put(key, "*"); - foundPut[0] = true; return super.visit(node); } }); 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 13b9455..a97b4cb 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 @@ -3429,4 +3429,232 @@ class EntryPointBindingExpanderTest { assertThat(variants).isEmpty(); } + + @Test + void shouldExpandCommandKeyFromStaticBlockPutAllWithMapOf(@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.putAll(Map.of( + "order.pay", OrderEvent.PAY, + "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 shouldExpandCommandKeyFromStaticBlockPutAllFromCrossClassMap(@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.putAll(CommandRoutes.SEED); + } + void trigger(String commandKey) { + StateMachine sm = new StateMachine(); + sm.sendEvent(ROUTES.get(commandKey)); + } + } + class CommandRoutes { + static final Map SEED = Map.of( + "order.pay", OrderEvent.PAY, + "order.ship", OrderEvent.SHIP + ); + } + 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 shouldExpandCommandKeyFromStaticInitializerMapPutEntriesWithStaticFinalKeys(@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 String PAY_KEY = "order.pay"; + private static final String SHIP_KEY = "order.ship"; + private static final Map ROUTES; + static { + ROUTES = new HashMap<>(); + ROUTES.put(PAY_KEY, OrderEvent.PAY); + ROUTES.put(SHIP_KEY, 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 shouldNotExpandCommandKeyFromStaticBlockPutAllWithRuntimeMap(@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.putAll(buildRoutes()); + } + static Map buildRoutes() { + Map routes = new HashMap<>(); + routes.put("order.pay", OrderEvent.PAY); + return routes; + } + 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(); + } }