From 12db9dbef48e6df2787e499b06c92a6d0b4f6ef8 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 18:30:34 +0200 Subject: [PATCH] Harden static map path binding for cross-class and ofEntries routes. Resolve Routes.ROUTES-style QualifiedName receivers and Map.ofEntries initializers so commandKey expansion works for typical route-table dispatch patterns. Co-authored-by: Cursor --- .../service/EntryPointBindingExpander.java | 80 ++++++++++++-- .../EntryPointBindingExpanderTest.java | 100 ++++++++++++++++++ 2 files changed, 169 insertions(+), 11 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 2acba86..82f47d0 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 @@ -619,20 +619,51 @@ public final class EntryPointBindingExpander { } String fieldName = null; TypeDeclaration declaringType = null; - if (receiver instanceof SimpleName simpleName) { - fieldName = simpleName.getIdentifier(); - declaringType = AstUtils.findEnclosingType(receiver); - } else if (receiver instanceof FieldAccess fieldAccess) { + if (receiver instanceof FieldAccess fieldAccess) { fieldName = fieldAccess.getName().getIdentifier(); - Expression qualifier = fieldAccess.getExpression(); + 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) { + declaringType = context.getTypeDeclaration(declaringClass.getQualifiedName()); + } + } + 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()); + } + } else if (qualifier instanceof QualifiedName qualifiedName) { + declaringType = context.getTypeDeclaration(qualifiedName.getFullyQualifiedName()); + } + } + } else if (receiver 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) { + declaringType = context.getTypeDeclaration(declaringClass.getQualifiedName()); + } + } + if (declaringType == null) { + declaringType = AstUtils.findEnclosingType(receiver); + } + } else if (receiver instanceof QualifiedName qualifiedName) { + 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()); } - } else if (qualifier instanceof QualifiedName qualifiedName) { - declaringType = context.getTypeDeclaration(qualifiedName.getFullyQualifiedName()); + } else if (qualifier instanceof QualifiedName typeQualifiedName) { + declaringType = context.getTypeDeclaration(typeQualifiedName.getFullyQualifiedName()); } } if (fieldName == null || declaringType == null) { @@ -662,13 +693,40 @@ public final class EntryPointBindingExpander { return null; } String factoryMethod = mapInvocation.getName().getIdentifier(); - if (!"of".equals(factoryMethod)) { - return null; - } if (!isMapFactoryReceiver(mapInvocation.getExpression())) { return null; } - return extractAlternatingLiteralPairs(mapInvocation.arguments()); + if ("of".equals(factoryMethod)) { + return extractAlternatingLiteralPairs(mapInvocation.arguments()); + } + if ("ofEntries".equals(factoryMethod)) { + return extractMapEntryCallArguments(mapInvocation.arguments()); + } + return null; + } + + private static Map extractMapEntryCallArguments(List arguments) { + Map entries = new LinkedHashMap<>(); + for (Object argObj : arguments) { + if (!(argObj instanceof MethodInvocation entryCall)) { + return null; + } + if (!"entry".equals(entryCall.getName().getIdentifier())) { + return null; + } + if (!isMapFactoryReceiver(entryCall.getExpression())) { + return null; + } + if (entryCall.arguments().size() != 2) { + return null; + } + Object keyObj = entryCall.arguments().get(0); + if (!(keyObj instanceof StringLiteral stringLiteral)) { + return null; + } + entries.put(stringLiteral.getLiteralValue(), "*"); + } + return entries.isEmpty() ? null : entries; } private static boolean isMapFactoryReceiver(Expression receiver) { 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 3fb493d..9f5c6fa 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 @@ -1886,4 +1886,104 @@ class EntryPointBindingExpanderTest { assertThat(variants.stream().map(v -> v.get("machineType")).distinct()) .containsExactlyInAnyOrder("ORDER", "DOCUMENT"); } + + @Test + void shouldExpandCommandKeyFromCrossClassStaticMapLookup(@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) { + StateMachine sm = new StateMachine(); + sm.sendEvent(Routes.ROUTES.get(commandKey)); + } + } + class Routes { + static final java.util.Map ROUTES = java.util.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 shouldExpandCommandKeyFromStaticMapOfEntriesLookup(@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 java.util.Map ROUTES = java.util.Map.ofEntries( + java.util.Map.entry("order.pay", OrderEvent.PAY), + java.util.Map.entry("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"); + } }