From 73f047e0aa0a1b764f973620b4aaca92c428ae1b Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 18:38:30 +0200 Subject: [PATCH] Expand if-equals path bindings through static string constants. Resolve ORDER.equals(machineType) and Objects.equals(CONST, param) by reusing compile-time static final string constant folding for both equals sides. Co-authored-by: Cursor --- .../service/EntryPointBindingExpander.java | 54 ++++++--- .../EntryPointBindingExpanderTest.java | 112 ++++++++++++++++++ 2 files changed, 152 insertions(+), 14 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 a8452ed..f19f9a7 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 @@ -1262,28 +1262,49 @@ public final class EntryPointBindingExpander { } Expression argument = (Expression) invocation.arguments().get(0); Expression receiver = invocation.getExpression(); + TypeDeclaration declaringType = ownerMethod != null + ? AstUtils.findEnclosingType(ownerMethod) + : null; - if (receiver instanceof StringLiteral stringLiteral + String receiverLiteral = resolveEqualsSideLiteral(receiver, declaringType, context); + if (receiverLiteral != null && argument instanceof SimpleName simpleName && paramName.equals(simpleName.getIdentifier())) { - return stringLiteral.getLiteralValue(); + return receiverLiteral; } - if (receiver instanceof StringLiteral stringLiteral + if (receiverLiteral != null && ownerMethod != null && context != null && switchSelectorReferencesParameter(argument, paramName, ownerMethod, context)) { - return stringLiteral.getLiteralValue(); + return receiverLiteral; } if (receiver instanceof SimpleName simpleName - && paramName.equals(simpleName.getIdentifier()) - && argument instanceof StringLiteral paramLiteral) { - return paramLiteral.getLiteralValue(); + && paramName.equals(simpleName.getIdentifier())) { + String argumentLiteral = resolveEqualsSideLiteral(argument, declaringType, context); + if (argumentLiteral != null) { + return argumentLiteral; + } } if (ownerMethod != null && context != null - && switchSelectorReferencesParameter(receiver, paramName, ownerMethod, context) - && argument instanceof StringLiteral paramLiteral) { - return paramLiteral.getLiteralValue(); + && switchSelectorReferencesParameter(receiver, paramName, ownerMethod, context)) { + String argumentLiteral = resolveEqualsSideLiteral(argument, declaringType, context); + if (argumentLiteral != null) { + return argumentLiteral; + } + } + return null; + } + + private static String resolveEqualsSideLiteral( + Expression expression, + TypeDeclaration declaringType, + CodebaseContext context) { + if (expression instanceof StringLiteral stringLiteral) { + return stringLiteral.getLiteralValue(); + } + if (declaringType != null && context != null) { + return resolveCompileTimeStringKey(expression, declaringType, context); } return null; } @@ -1304,13 +1325,18 @@ public final class EntryPointBindingExpander { } Expression left = (Expression) invocation.arguments().get(0); Expression right = (Expression) invocation.arguments().get(1); - if (left instanceof StringLiteral stringLiteral + TypeDeclaration declaringType = ownerMethod != null + ? AstUtils.findEnclosingType(ownerMethod) + : null; + String leftLiteral = resolveEqualsSideLiteral(left, declaringType, context); + if (leftLiteral != null && parameterExpressionReferencesBinding(right, paramName, ownerMethod, context)) { - return stringLiteral.getLiteralValue(); + return leftLiteral; } - if (right instanceof StringLiteral stringLiteral + String rightLiteral = resolveEqualsSideLiteral(right, declaringType, context); + if (rightLiteral != null && parameterExpressionReferencesBinding(left, paramName, ownerMethod, context)) { - return stringLiteral.getLiteralValue(); + return rightLiteral; } return null; } 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 c79fe64..07d448f 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 @@ -2585,4 +2585,116 @@ class EntryPointBindingExpanderTest { assertThat(variants).extracting(map -> map.get("commandKey")) .containsExactlyInAnyOrder("order.pay", "order.ship"); } + + @Test + void shouldExpandMachineTypeFromIfEqualsOnStaticFinalConstant(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + public class MachineController { + StateMachineDispatcher dispatcher; + public void transition(String machineType, String event) { + dispatcher.dispatch(machineType, event); + } + } + class StateMachineDispatcher { + private static final String ORDER = "ORDER"; + private static final String DOCUMENT = "DOCUMENT"; + void dispatch(String machineType, String eventString) { + if (ORDER.equals(machineType)) { + OrderEvent.valueOf(eventString.toUpperCase()); + } else if (DOCUMENT.equalsIgnoreCase(machineType)) { + DocumentEvent.valueOf(eventString.toUpperCase()); + } + } + } + enum OrderEvent { PAY, SHIP } + enum DocumentEvent { SUBMIT, APPROVE } + """; + 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.MachineController") + .methodName("transition") + .name("POST /api/machine/{machineType}/transition/{event}") + .parameters(List.of( + EntryPoint.Parameter.builder() + .name("machineType") + .type("String") + .annotations(List.of("PathVariable")) + .build(), + EntryPoint.Parameter.builder() + .name("event") + .type("String") + .annotations(List.of("PathVariable")) + .build())) + .build(); + + List> variants = + EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph); + + assertThat(variants.stream().map(v -> v.get("machineType")).distinct()) + .containsExactlyInAnyOrder("ORDER", "DOCUMENT"); + } + + @Test + void shouldExpandCommandKeyFromSwitchExpressionWithCrossClassCaseConstants(@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 { + void trigger(String commandKey) { + DomainCommand command = switch (commandKey) { + case CommandKeys.PAY_KEY -> DomainCommand.ORDER_PAY; + case CommandKeys.SHIP_KEY -> DomainCommand.ORDER_SHIP; + default -> throw new IllegalArgumentException("Unknown command: " + commandKey); + }; + StateMachine sm = new StateMachine(); + sm.sendEvent(command); + } + } + class CommandKeys { + static final String PAY_KEY = "order.pay"; + static final String SHIP_KEY = "order.ship"; + } + 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"); + } }