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 a4fd9ad..a8452ed 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 @@ -779,6 +779,14 @@ public final class EntryPointBindingExpander { Expression expression, TypeDeclaration defaultDeclaringType, CodebaseContext context) { + return resolveCompileTimeStringKey(expression, defaultDeclaringType, context, new LinkedHashSet<>()); + } + + private static String resolveCompileTimeStringKey( + Expression expression, + TypeDeclaration defaultDeclaringType, + CodebaseContext context, + Set visiting) { if (expression instanceof StringLiteral stringLiteral) { return stringLiteral.getLiteralValue(); } @@ -834,7 +842,7 @@ public final class EntryPointBindingExpander { if (fieldName == null) { return null; } - return resolveStaticFinalStringLiteralField(fieldDeclaringType, fieldName); + return resolveStaticFinalStringLiteralField(fieldDeclaringType, fieldName, context, visiting); } private static TypeDeclaration resolveTypeForFieldQualifier( @@ -862,10 +870,19 @@ public final class EntryPointBindingExpander { return null; } - private static String resolveStaticFinalStringLiteralField(TypeDeclaration typeDecl, String fieldName) { - if (typeDecl == null) { + private static String resolveStaticFinalStringLiteralField( + TypeDeclaration typeDecl, + String fieldName, + CodebaseContext context, + Set visiting) { + if (typeDecl == null || context == null) { return null; } + String visitKey = context.getFqn(typeDecl) + "#" + fieldName; + if (visiting.contains(visitKey)) { + return null; + } + visiting.add(visitKey); for (FieldDeclaration field : typeDecl.getFields()) { if (!isStaticField(field)) { continue; @@ -877,7 +894,7 @@ public final class EntryPointBindingExpander { if (initializer instanceof StringLiteral stringLiteral) { return stringLiteral.getLiteralValue(); } - return null; + return resolveCompileTimeStringKey(initializer, typeDecl, context, visiting); } } } @@ -1137,9 +1154,15 @@ public final class EntryPointBindingExpander { } for (Object statementObj : statements) { if (statementObj instanceof SwitchCase switchCase && !switchCase.isDefault()) { + TypeDeclaration declaringType = ownerMethod != null + ? AstUtils.findEnclosingType(ownerMethod) + : null; for (Object exprObj : switchCase.expressions()) { - if (exprObj instanceof StringLiteral stringLiteral) { - cases.add(stringLiteral.getLiteralValue()); + if (exprObj instanceof Expression caseExpression) { + String key = resolveCompileTimeStringKey(caseExpression, declaringType, context); + if (key != null) { + cases.add(key); + } } } } 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 f333b9e..c79fe64 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 @@ -2478,4 +2478,111 @@ class EntryPointBindingExpanderTest { assertThat(variants).isEmpty(); } + + @Test + void shouldExpandCommandKeyFromSwitchExpressionWithStaticFinalCaseConstants(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + 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"; + void trigger(String commandKey) { + DomainCommand command = switch (commandKey) { + case PAY_KEY -> DomainCommand.ORDER_PAY; + case SHIP_KEY -> DomainCommand.ORDER_SHIP; + default -> throw new IllegalArgumentException("Unknown command: " + commandKey); + }; + StateMachine sm = new StateMachine(); + sm.sendEvent(command); + } + } + enum DomainCommand { ORDER_PAY, ORDER_SHIP } + class StateMachine { void sendEvent(DomainCommand command) {} } + """; + 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 shouldExpandCommandKeyFromChainedStaticFinalStringConstantMapKeys(@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 String PAY_BASE = "order.pay"; + private static final String PAY_KEY = PAY_BASE; + private static final String SHIP_BASE = "order.ship"; + private static final String SHIP_KEY = SHIP_BASE; + private static final Map ROUTES = Map.of( + PAY_KEY, OrderEvent.PAY, + 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"); + } }