From b9db614cb1b454890b5d8f1fb3a4c839f52868cb Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 18:34:59 +0200 Subject: [PATCH] Expand map route keys from static final string constants. Resolve Map.of keys through compile-time static final String fields so route tables can use named constants while still failing closed on computed or non-static keys. Co-authored-by: Cursor --- .../service/EntryPointBindingExpander.java | 162 ++++++++++++++++-- .../EntryPointBindingExpanderTest.java | 109 +++++++++++- 2 files changed, 255 insertions(+), 16 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 5e22140..a4fd9ad 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 @@ -613,7 +613,8 @@ public final class EntryPointBindingExpander { if (receiver == null) { return null; } - Map direct = extractMapOfLiteralEntries(receiver); + Map direct = extractMapOfLiteralEntries( + receiver, AstUtils.findEnclosingType(receiver), context); if (direct != null) { return direct; } @@ -675,10 +676,13 @@ public final class EntryPointBindingExpander { if (fieldName == null || declaringType == null) { return null; } - return extractStaticFieldMapInitializer(declaringType, fieldName); + return extractStaticFieldMapInitializer(declaringType, fieldName, context); } - private static Map extractStaticFieldMapInitializer(TypeDeclaration typeDecl, String fieldName) { + private static Map extractStaticFieldMapInitializer( + TypeDeclaration typeDecl, + String fieldName, + CodebaseContext context) { for (FieldDeclaration field : typeDecl.getFields()) { for (Object fragmentObj : field.fragments()) { if (fragmentObj instanceof VariableDeclarationFragment fragment @@ -687,14 +691,17 @@ public final class EntryPointBindingExpander { if (initializer == null) { return null; } - return extractMapOfLiteralEntries(initializer); + return extractMapOfLiteralEntries(initializer, typeDecl, context); } } } return null; } - private static Map extractMapOfLiteralEntries(Expression expression) { + private static Map extractMapOfLiteralEntries( + Expression expression, + TypeDeclaration declaringType, + CodebaseContext context) { if (!(expression instanceof MethodInvocation mapInvocation)) { return null; } @@ -703,15 +710,18 @@ public final class EntryPointBindingExpander { return null; } if ("of".equals(factoryMethod)) { - return extractAlternatingLiteralPairs(mapInvocation.arguments()); + return extractAlternatingLiteralPairs(mapInvocation.arguments(), declaringType, context); } if ("ofEntries".equals(factoryMethod)) { - return extractMapEntryCallArguments(mapInvocation.arguments()); + return extractMapEntryCallArguments(mapInvocation.arguments(), declaringType, context); } return null; } - private static Map extractMapEntryCallArguments(List arguments) { + private static Map extractMapEntryCallArguments( + List arguments, + TypeDeclaration declaringType, + CodebaseContext context) { Map entries = new LinkedHashMap<>(); for (Object argObj : arguments) { if (!(argObj instanceof MethodInvocation entryCall)) { @@ -726,11 +736,12 @@ public final class EntryPointBindingExpander { if (entryCall.arguments().size() != 2) { return null; } - Object keyObj = entryCall.arguments().get(0); - if (!(keyObj instanceof StringLiteral stringLiteral)) { + String key = resolveCompileTimeStringKey( + (Expression) entryCall.arguments().get(0), declaringType, context); + if (key == null) { return null; } - entries.put(stringLiteral.getLiteralValue(), "*"); + entries.put(key, "*"); } return entries.isEmpty() ? null : entries; } @@ -746,21 +757,142 @@ public final class EntryPointBindingExpander { return false; } - private static Map extractAlternatingLiteralPairs(List arguments) { + private static Map extractAlternatingLiteralPairs( + List arguments, + TypeDeclaration declaringType, + CodebaseContext context) { if (arguments.size() < 2) { return null; } Map entries = new LinkedHashMap<>(); for (int i = 0; i + 1 < arguments.size(); i += 2) { - Object keyObj = arguments.get(i); - if (!(keyObj instanceof StringLiteral stringLiteral)) { + String key = resolveCompileTimeStringKey((Expression) arguments.get(i), declaringType, context); + if (key == null) { return null; } - entries.put(stringLiteral.getLiteralValue(), "*"); + entries.put(key, "*"); } return entries.isEmpty() ? null : entries; } + private static String resolveCompileTimeStringKey( + Expression expression, + TypeDeclaration defaultDeclaringType, + CodebaseContext context) { + if (expression instanceof StringLiteral stringLiteral) { + return stringLiteral.getLiteralValue(); + } + String fieldName = null; + TypeDeclaration fieldDeclaringType = defaultDeclaringType; + if (expression instanceof SimpleName simpleName) { + fieldName = simpleName.getIdentifier(); + org.eclipse.jdt.core.dom.IBinding binding = simpleName.resolveBinding(); + if (binding instanceof org.eclipse.jdt.core.dom.IVariableBinding variableBinding + && variableBinding.isField()) { + org.eclipse.jdt.core.dom.ITypeBinding declaringClass = variableBinding.getDeclaringClass(); + if (declaringClass != null && context != null) { + TypeDeclaration resolved = context.getTypeDeclaration(declaringClass.getQualifiedName()); + if (resolved != null) { + fieldDeclaringType = resolved; + } + } + } else if (defaultDeclaringType != null && context != null) { + ASTNode root = defaultDeclaringType.getRoot(); + if (root instanceof CompilationUnit compilationUnit) { + TypeDeclaration staticImport = context.resolveStaticImport(fieldName, compilationUnit); + if (staticImport != null) { + fieldDeclaringType = staticImport; + } + } + } + } else if (expression instanceof FieldAccess fieldAccess) { + fieldName = fieldAccess.getName().getIdentifier(); + org.eclipse.jdt.core.dom.IVariableBinding fieldBinding = fieldAccess.resolveFieldBinding(); + if (fieldBinding != null && fieldBinding.isField()) { + org.eclipse.jdt.core.dom.ITypeBinding declaringClass = fieldBinding.getDeclaringClass(); + if (declaringClass != null && context != null) { + TypeDeclaration resolved = context.getTypeDeclaration(declaringClass.getQualifiedName()); + if (resolved != null) { + fieldDeclaringType = resolved; + } + } + } else { + TypeDeclaration resolved = resolveTypeForFieldQualifier( + fieldAccess.getExpression(), defaultDeclaringType, context); + if (resolved != null) { + fieldDeclaringType = resolved; + } + } + } else if (expression instanceof QualifiedName qualifiedName) { + fieldName = qualifiedName.getName().getIdentifier(); + TypeDeclaration resolved = resolveTypeForFieldQualifier( + qualifiedName.getQualifier(), defaultDeclaringType, context); + if (resolved != null) { + fieldDeclaringType = resolved; + } + } + if (fieldName == null) { + return null; + } + return resolveStaticFinalStringLiteralField(fieldDeclaringType, fieldName); + } + + private static TypeDeclaration resolveTypeForFieldQualifier( + Expression qualifier, + TypeDeclaration defaultDeclaringType, + CodebaseContext context) { + if (context == null) { + return null; + } + if (qualifier instanceof SimpleName typeName) { + if (defaultDeclaringType != null) { + ASTNode root = defaultDeclaringType.getRoot(); + if (root instanceof CompilationUnit compilationUnit) { + TypeDeclaration resolved = context.getTypeDeclaration(typeName.getIdentifier(), compilationUnit); + if (resolved != null) { + return resolved; + } + } + } + return context.getTypeDeclaration(typeName.getIdentifier()); + } + if (qualifier instanceof QualifiedName qualifiedName) { + return context.getTypeDeclaration(qualifiedName.getFullyQualifiedName()); + } + return null; + } + + private static String resolveStaticFinalStringLiteralField(TypeDeclaration typeDecl, String fieldName) { + if (typeDecl == null) { + return null; + } + for (FieldDeclaration field : typeDecl.getFields()) { + if (!isStaticField(field)) { + continue; + } + for (Object fragmentObj : field.fragments()) { + if (fragmentObj instanceof VariableDeclarationFragment fragment + && fieldName.equals(fragment.getName().getIdentifier())) { + Expression initializer = fragment.getInitializer(); + if (initializer instanceof StringLiteral stringLiteral) { + return stringLiteral.getLiteralValue(); + } + return null; + } + } + } + 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 e8774fb..f333b9e 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 @@ -2322,7 +2322,7 @@ class EntryPointBindingExpanderTest { } @Test - void shouldNotExpandCommandKeyWhenMapKeysAreNonLiteralConstants(@TempDir Path tempDir) throws Exception { + void shouldExpandCommandKeyFromStaticFinalStringConstantMapKeys(@TempDir Path tempDir) throws Exception { String source = """ package com.example; import java.util.Map; @@ -2355,6 +2355,113 @@ class EntryPointBindingExpanderTest { 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 shouldExpandCommandKeyFromCrossClassStaticFinalStringConstantMapKeys(@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( + CommandKeys.PAY_KEY, OrderEvent.PAY, + CommandKeys.SHIP_KEY, OrderEvent.SHIP); + void trigger(String commandKey) { + StateMachine sm = new StateMachine(); + sm.sendEvent(ROUTES.get(commandKey)); + } + } + class CommandKeys { + static final String PAY_KEY = "order.pay"; + static final String SHIP_KEY = "order.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 shouldNotExpandCommandKeyWhenMapKeysAreComputed(@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( + prefix("order.pay"), OrderEvent.PAY, + prefix("order.ship"), OrderEvent.SHIP); + void trigger(String commandKey) { + StateMachine sm = new StateMachine(); + sm.sendEvent(ROUTES.get(commandKey)); + } + static String prefix(String value) { + return value; + } + } + 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")