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 <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 19:10:48 +02:00
parent 3e0f8f18b8
commit d19d4f20ef
3 changed files with 158 additions and 5 deletions

View File

@@ -714,21 +714,36 @@ public class CodebaseContext {
} }
private String resolveSuperclassFqn(TypeDeclaration td) { private String resolveSuperclassFqn(TypeDeclaration td) {
String bindingSuper = null;
ITypeBinding binding = td.resolveBinding(); ITypeBinding binding = td.resolveBinding();
if (binding != null) { if (binding != null) {
ITypeBinding superBinding = binding.getSuperclass(); ITypeBinding superBinding = binding.getSuperclass();
if (superBinding != null) { if (superBinding != null) {
return superBinding.getErasure().getQualifiedName(); bindingSuper = superBinding.getErasure().getQualifiedName();
} }
} }
Type superType = td.getSuperclassType(); Type superType = td.getSuperclassType();
if (superType == null) return null; if (superType == null) {
return bindingSuper;
}
String superName = extractTypeName(superType); String superName = extractTypeName(superType);
CompilationUnit cu = (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null; CompilationUnit cu = (td.getRoot() instanceof CompilationUnit) ? (CompilationUnit) td.getRoot() : null;
TypeDeclaration superTd = getTypeDeclaration(superName, cu); 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) { private String extractTypeName(Type type) {

View File

@@ -4963,4 +4963,65 @@ class EntryPointBindingExpanderTest {
assertThat(variants).isEmpty(); 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<String, String> 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<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> 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<Map<String, String>> variants =
EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph);
assertThat(variants).extracting(map -> map.get("commandKey"))
.containsExactlyInAnyOrder("order.pay", "order.ship");
}
} }

View File

@@ -806,4 +806,81 @@ class MultiModulePathBindingExpanderTest {
assertThat(variants).extracting(map -> map.get("commandKey")) assertThat(variants).extracting(map -> map.get("commandKey"))
.containsExactlyInAnyOrder("order.pay", "order.ship"); .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<String, String> 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<Path> scanPaths = graph.resolveScanPaths(coreModule, false);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(scanPaths, Collections.emptySet());
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> 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<Map<String, String>> variants =
EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);
assertThat(variants).extracting(map -> map.get("commandKey"))
.containsExactlyInAnyOrder("order.pay", "order.ship");
}
} }