From d19d4f20efecf2e05337a29ed275d0720b2bedfd Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 19:10:48 +0200 Subject: [PATCH] Prefer import-aware superclass FQN when JDT binding mis-resolves. Fix inherited static-block map expansion across compilation units by resolving superclass types from imports when binding points at the wrong package. Co-authored-by: Cursor --- .../ast/common/CodebaseContext.java | 25 ++++-- .../EntryPointBindingExpanderTest.java | 61 +++++++++++++++ .../MultiModulePathBindingExpanderTest.java | 77 +++++++++++++++++++ 3 files changed, 158 insertions(+), 5 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index 4251643..678faf1 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -714,21 +714,36 @@ public class CodebaseContext { } private String resolveSuperclassFqn(TypeDeclaration td) { + String bindingSuper = null; ITypeBinding binding = td.resolveBinding(); if (binding != null) { ITypeBinding superBinding = binding.getSuperclass(); if (superBinding != null) { - return superBinding.getErasure().getQualifiedName(); + bindingSuper = superBinding.getErasure().getQualifiedName(); } } - + Type superType = td.getSuperclassType(); - if (superType == null) return null; - + if (superType == null) { + return bindingSuper; + } + String superName = extractTypeName(superType); CompilationUnit cu = (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null; TypeDeclaration superTd = getTypeDeclaration(superName, cu); - return superTd != null ? getFqn(superTd) : superName; + String astSuper = superTd != null ? getFqn(superTd) : null; + + if (astSuper != null) { + if (bindingSuper == null || !astSuper.equals(bindingSuper)) { + if (bindingSuper == null + || getTypeDeclaration(bindingSuper) == null + || classes.containsKey(astSuper)) { + return astSuper; + } + } + return astSuper; + } + return bindingSuper != null ? bindingSuper : superName; } private String extractTypeName(Type type) { 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 21759a2..a69166e 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 @@ -4963,4 +4963,65 @@ class EntryPointBindingExpanderTest { assertThat(variants).isEmpty(); } + + @Test + void shouldExpandCommandKeyFromInheritedStaticBlockMapInSeparateCompilationUnit(@TempDir Path tempDir) throws Exception { + Files.createDirectories(tempDir.resolve("com/example/routes")); + Files.writeString(tempDir.resolve("com/example/routes/BaseRoutes.java"), """ + package com.example.routes; + import java.util.HashMap; + import java.util.Map; + public abstract class BaseRoutes { + protected static final Map ROUTES; + static { + ROUTES = new HashMap<>(); + ROUTES.put("order.pay", "PAY"); + ROUTES.put("order.ship", "SHIP"); + } + } + """); + Files.writeString(tempDir.resolve("com/example/App.java"), """ + package com.example; + import com.example.routes.BaseRoutes; + import java.util.Map; + public class GenericCommandController { + OrderGateway gateway; + public void execute(String commandKey) { + gateway.trigger(commandKey); + } + } + class OrderGateway extends BaseRoutes { + void trigger(String commandKey) { + StateMachine sm = new StateMachine(); + sm.sendEvent(ROUTES.get(commandKey)); + } + } + class StateMachine { void sendEvent(String event) {} } + """); + + 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"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/MultiModulePathBindingExpanderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/MultiModulePathBindingExpanderTest.java index cdd6988..29d9507 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/MultiModulePathBindingExpanderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/MultiModulePathBindingExpanderTest.java @@ -806,4 +806,81 @@ class MultiModulePathBindingExpanderTest { assertThat(variants).extracting(map -> map.get("commandKey")) .containsExactlyInAnyOrder("order.pay", "order.ship"); } + + @Test + void shouldExpandCommandKeyFromInheritedStaticBlockMapInSiblingModule(@TempDir Path tempDir) throws Exception { + Path root = tempDir.resolve("order-system"); + Files.createDirectories(root); + Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'"); + + Path apiModule = root.resolve("api-module"); + Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes")); + Files.writeString(apiModule.resolve("build.gradle"), ""); + Files.writeString(apiModule.resolve("src/main/java/com/example/routes/BaseRoutes.java"), """ + package com.example.routes; + import java.util.HashMap; + import java.util.Map; + public abstract class BaseRoutes { + protected static final Map ROUTES; + static { + ROUTES = new HashMap<>(); + ROUTES.put("order.pay", "OrderEvent.PAY"); + ROUTES.put("order.ship", "OrderEvent.SHIP"); + } + } + """); + + Path coreModule = root.resolve("core-module"); + Files.createDirectories(coreModule.resolve("src/main/java/com/example")); + Files.writeString(coreModule.resolve("build.gradle"), + "dependencies { implementation project(':api-module') }"); + Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """ + package com.example; + import com.example.routes.BaseRoutes; + public class GenericCommandController { + OrderGateway gateway; + public void execute(String commandKey) { + gateway.trigger(commandKey); + } + } + class OrderGateway extends BaseRoutes { + void trigger(String commandKey) { + OrderEvent event = OrderEvent.valueOf(ROUTES.get(commandKey)); + StateMachine sm = new StateMachine(); + sm.sendEvent(event); + } + } + enum OrderEvent { PAY, SHIP } + class StateMachine { void sendEvent(OrderEvent event) {} } + """); + + SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver(); + ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule); + Set scanPaths = graph.resolveScanPaths(coreModule, false); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.scan(scanPaths, Collections.emptySet()); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + Map> callGraph = + 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, callGraph); + + assertThat(variants).extracting(map -> map.get("commandKey")) + .containsExactlyInAnyOrder("order.pay", "order.ship"); + } }