From 9455a2c8a9c8bfebccc83bcc50ca04fd9a31b2f7 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 18:56:11 +0200 Subject: [PATCH] Resolve inherited and interface static route map fields. Walk superclass and interface hierarchies when locating compile-time map initializers for Map.get path binding expansion. Co-authored-by: Cursor --- .../service/EntryPointBindingExpander.java | 59 +++++++ .../EntryPointBindingExpanderTest.java | 159 ++++++++++++++++++ 2 files changed, 218 insertions(+) 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 6413435..1a75b21 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 @@ -705,6 +705,65 @@ public final class EntryPointBindingExpander { TypeDeclaration typeDecl, String fieldName, CodebaseContext context) { + for (TypeDeclaration current = typeDecl; current != null; current = superTypeDeclaration(current, context)) { + Map entries = extractStaticFieldMapInitializerOnType(current, fieldName, context); + if (entries != null) { + return entries; + } + } + return extractStaticFieldMapInitializerFromInterfaces(typeDecl, fieldName, context, new LinkedHashSet<>()); + } + + private static Map extractStaticFieldMapInitializerFromInterfaces( + TypeDeclaration typeDecl, + String fieldName, + CodebaseContext context, + Set visited) { + if (typeDecl == null) { + return null; + } + String typeFqn = context.getFqn(typeDecl); + if (typeFqn != null && !visited.add(typeFqn)) { + return null; + } + CompilationUnit compilationUnit = typeDecl.getRoot() instanceof CompilationUnit cu ? cu : null; + for (Object interfaceTypeObj : typeDecl.superInterfaceTypes()) { + if (!(interfaceTypeObj instanceof Type interfaceType)) { + continue; + } + String interfaceName = interfaceType.toString(); + TypeDeclaration interfaceDecl = context.getTypeDeclaration(interfaceName, compilationUnit); + if (interfaceDecl == null) { + interfaceDecl = context.getTypeDeclaration(interfaceName); + } + if (interfaceDecl == null) { + continue; + } + Map entries = extractStaticFieldMapInitializerOnType(interfaceDecl, fieldName, context); + if (entries != null) { + return entries; + } + Map inherited = extractStaticFieldMapInitializerFromInterfaces( + interfaceDecl, fieldName, context, visited); + if (inherited != null) { + return inherited; + } + } + return null; + } + + private static TypeDeclaration superTypeDeclaration(TypeDeclaration typeDecl, CodebaseContext context) { + String superFqn = context.getSuperclassFqn(typeDecl); + if (superFqn == null || "java.lang.Object".equals(superFqn)) { + return null; + } + return context.getTypeDeclaration(superFqn); + } + + private static Map extractStaticFieldMapInitializerOnType( + TypeDeclaration typeDecl, + String fieldName, + CodebaseContext context) { for (FieldDeclaration field : typeDecl.getFields()) { for (Object fragmentObj : field.fragments()) { if (fragmentObj instanceof VariableDeclarationFragment fragment 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 3ba4843..9368575 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 @@ -4592,4 +4592,163 @@ class EntryPointBindingExpanderTest { assertThat(variants).isEmpty(); } + + @Test + void shouldExpandCommandKeyFromInheritedStaticMapField(@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); + } + } + abstract class BaseRoutes { + protected static final Map ROUTES = Map.of( + "order.pay", OrderEvent.PAY, + "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 shouldExpandCommandKeyFromInterfaceStaticMapField(@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); + } + } + interface RouteSource { + Map ROUTES = Map.of( + "order.pay", OrderEvent.PAY, + "order.ship", OrderEvent.SHIP + ); + } + class OrderGateway implements RouteSource { + 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 shouldExpandCommandKeyFromQualifiedInheritedStaticMapField(@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); + } + } + abstract class BaseRoutes { + protected static final Map ROUTES = Map.of( + "order.pay", OrderEvent.PAY, + "order.ship", OrderEvent.SHIP + ); + } + class OrderGateway extends BaseRoutes { + void trigger(String commandKey) { + StateMachine sm = new StateMachine(); + sm.sendEvent(BaseRoutes.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"); + } }