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 1a75b21..8a85ed4 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 @@ -60,7 +60,7 @@ public final class EntryPointBindingExpander { if (!isPathOrQueryVariable(parameter)) { continue; } - if (isPlaceholderResolvedInPath(entryPoint, parameter.getName())) { + if (!isRequestParam(parameter) && isPlaceholderResolvedInPath(entryPoint, parameter.getName())) { continue; } List> expanded = new ArrayList<>(); @@ -119,7 +119,8 @@ public final class EntryPointBindingExpander { if (path != null) { metadata.put("path", path.replace(placeholder, binding.getValue())); } - } else if (isBooleanBindingValue(binding.getValue())) { + } else if (isRequestParamParameter(entryPoint, binding.getKey()) + || isBooleanBindingValue(binding.getValue())) { if (querySuffix.isEmpty()) { querySuffix.append('?'); } else { @@ -282,6 +283,25 @@ public final class EntryPointBindingExpander { .anyMatch(a -> "PathVariable".equals(a) || "RequestParam".equals(a)); } + private static boolean isRequestParam(EntryPoint.Parameter parameter) { + if (parameter.getAnnotations() == null) { + return false; + } + return parameter.getAnnotations().stream().anyMatch("RequestParam"::equals); + } + + private static boolean isRequestParamParameter(EntryPoint entryPoint, String paramName) { + if (entryPoint == null || entryPoint.getParameters() == null) { + return false; + } + for (EntryPoint.Parameter parameter : entryPoint.getParameters()) { + if (paramName.equals(parameter.getName()) && isRequestParam(parameter)) { + return true; + } + } + return false; + } + private static List resolveBindingCasesForParameter( String startMethod, String paramName, @@ -698,9 +718,31 @@ public final class EntryPointBindingExpander { if (fieldName == null || declaringType == null) { return null; } + if (!isStaticMapFieldReceiver(receiver)) { + return null; + } return extractStaticFieldMapInitializer(declaringType, fieldName, context); } + private static boolean isStaticMapFieldReceiver(Expression receiver) { + if (receiver instanceof FieldAccess fieldAccess) { + org.eclipse.jdt.core.dom.IVariableBinding fieldBinding = fieldAccess.resolveFieldBinding(); + return fieldBinding == null || Modifier.isStatic(fieldBinding.getModifiers()); + } + if (receiver instanceof SimpleName simpleName) { + org.eclipse.jdt.core.dom.IBinding binding = simpleName.resolveBinding(); + if (binding instanceof org.eclipse.jdt.core.dom.IVariableBinding variableBinding + && variableBinding.isField()) { + return Modifier.isStatic(variableBinding.getModifiers()); + } + return true; + } + if (receiver instanceof QualifiedName) { + return true; + } + return false; + } + private static Map extractStaticFieldMapInitializer( TypeDeclaration typeDecl, String fieldName, @@ -765,6 +807,9 @@ public final class EntryPointBindingExpander { String fieldName, CodebaseContext context) { for (FieldDeclaration field : typeDecl.getFields()) { + if (!isStaticField(field)) { + continue; + } for (Object fragmentObj : field.fragments()) { if (fragmentObj instanceof VariableDeclarationFragment fragment && fieldName.equals(fragment.getName().getIdentifier())) { @@ -792,6 +837,19 @@ public final class EntryPointBindingExpander { return null; } + private static boolean isStaticField(FieldDeclaration field) { + ASTNode parent = field.getParent(); + if (parent instanceof TypeDeclaration typeDeclaration && typeDeclaration.isInterface()) { + return true; + } + for (Object modifierObj : field.modifiers()) { + if (modifierObj instanceof Modifier modifier && modifier.isStatic()) { + return true; + } + } + return false; + } + private static Map extractStaticBlockMapPutEntries( TypeDeclaration typeDecl, String fieldName, @@ -1322,15 +1380,6 @@ public final class EntryPointBindingExpander { return null; } - private static boolean isStaticField(FieldDeclaration field) { - for (Object modifierObj : field.modifiers()) { - if (modifierObj instanceof Modifier modifier && modifier.isStatic()) { - return true; - } - } - return false; - } - private static boolean isValueOfSiteCompatibleWithBindings( MethodInvocation valueOfInvocation, Map partialBindings, 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 9368575..21759a2 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 @@ -4751,4 +4751,216 @@ class EntryPointBindingExpanderTest { assertThat(variants).extracting(map -> map.get("commandKey")) .containsExactlyInAnyOrder("order.pay", "order.ship"); } + + @Test + void shouldExpandCommandKeyFromStaticMethodMapReceiver(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + import java.util.Map; + public class GenericCommandController { + OrderGateway gateway; + public void execute(String commandKey) { + gateway.trigger(commandKey); + } + } + class OrderGateway { + static Map routes() { + return 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 shouldExpandCommandKeyFromInheritedStaticBlockMap(@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); + } + } + abstract class BaseRoutes { + protected static final Map ROUTES; + static { + ROUTES = new HashMap<>(); + ROUTES.put("order.pay", OrderEvent.PAY); + ROUTES.put("order.ship", OrderEvent.SHIP); + } + } + class OrderGateway extends BaseRoutes { + 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 shouldExpandRequestParamFromStaticMapLookup(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + import java.util.Map; + public class GenericCommandController { + OrderGateway gateway; + public void execute(String commandKey) { + gateway.trigger(commandKey); + } + } + class OrderGateway { + private static final Map ROUTES = 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") + .parameters(List.of(EntryPoint.Parameter.builder() + .name("commandKey") + .type("String") + .annotations(List.of("RequestParam")) + .build())) + .build(); + + List> variants = + EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph); + + assertThat(variants).extracting(map -> map.get("commandKey")) + .containsExactlyInAnyOrder("order.pay", "order.ship"); + assertThat(EntryPointBindingExpander.withResolvedPath(entryPoint, variants.get(0)).getName()) + .isEqualTo("POST /api/commands?commandKey=order.pay"); + } + + @Test + void shouldNotExpandCommandKeyFromInstanceFieldMap(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + import java.util.Map; + public class GenericCommandController { + OrderGateway gateway; + public void execute(String commandKey) { + gateway.trigger(commandKey); + } + } + class OrderGateway { + private final Map routes = 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).isEmpty(); + } }