Harden cross-module map lookup and add sibling-module regression tests.
Resolve static map field declaring types via import-aware lookup when JDT binding uses the wrong package, and cover qualified inherited maps, static factory receivers, and RequestParam expansion across modules. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -672,9 +672,7 @@ public final class EntryPointBindingExpander {
|
|||||||
org.eclipse.jdt.core.dom.IVariableBinding fieldBinding = fieldAccess.resolveFieldBinding();
|
org.eclipse.jdt.core.dom.IVariableBinding fieldBinding = fieldAccess.resolveFieldBinding();
|
||||||
if (fieldBinding != null && fieldBinding.isField()) {
|
if (fieldBinding != null && fieldBinding.isField()) {
|
||||||
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = fieldBinding.getDeclaringClass();
|
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = fieldBinding.getDeclaringClass();
|
||||||
if (declaringClass != null) {
|
declaringType = resolveFieldDeclaringType(declaringClass, compilationUnit, context);
|
||||||
declaringType = context.getTypeDeclaration(declaringClass.getQualifiedName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (declaringType == null) {
|
if (declaringType == null) {
|
||||||
Expression qualifier = fieldAccess.getExpression();
|
Expression qualifier = fieldAccess.getExpression();
|
||||||
@@ -693,9 +691,7 @@ public final class EntryPointBindingExpander {
|
|||||||
if (binding instanceof org.eclipse.jdt.core.dom.IVariableBinding variableBinding
|
if (binding instanceof org.eclipse.jdt.core.dom.IVariableBinding variableBinding
|
||||||
&& variableBinding.isField()) {
|
&& variableBinding.isField()) {
|
||||||
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = variableBinding.getDeclaringClass();
|
org.eclipse.jdt.core.dom.ITypeBinding declaringClass = variableBinding.getDeclaringClass();
|
||||||
if (declaringClass != null) {
|
declaringType = resolveFieldDeclaringType(declaringClass, compilationUnit, context);
|
||||||
declaringType = context.getTypeDeclaration(declaringClass.getQualifiedName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (declaringType == null && compilationUnit != null) {
|
if (declaringType == null && compilationUnit != null) {
|
||||||
declaringType = context.resolveStaticImport(fieldName, compilationUnit);
|
declaringType = context.resolveStaticImport(fieldName, compilationUnit);
|
||||||
@@ -724,6 +720,27 @@ public final class EntryPointBindingExpander {
|
|||||||
return extractStaticFieldMapInitializer(declaringType, fieldName, context);
|
return extractStaticFieldMapInitializer(declaringType, fieldName, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TypeDeclaration resolveFieldDeclaringType(
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding declaringClass,
|
||||||
|
CompilationUnit compilationUnit,
|
||||||
|
CodebaseContext context) {
|
||||||
|
if (declaringClass == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding erased = declaringClass.getErasure();
|
||||||
|
TypeDeclaration typeDeclaration = context.getTypeDeclaration(erased.getQualifiedName());
|
||||||
|
if (typeDeclaration != null) {
|
||||||
|
return typeDeclaration;
|
||||||
|
}
|
||||||
|
if (compilationUnit != null) {
|
||||||
|
typeDeclaration = context.getTypeDeclaration(erased.getName(), compilationUnit);
|
||||||
|
if (typeDeclaration != null) {
|
||||||
|
return typeDeclaration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context.getTypeDeclaration(erased.getName());
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isStaticMapFieldReceiver(Expression receiver) {
|
private static boolean isStaticMapFieldReceiver(Expression receiver) {
|
||||||
if (receiver instanceof FieldAccess fieldAccess) {
|
if (receiver instanceof FieldAccess fieldAccess) {
|
||||||
org.eclipse.jdt.core.dom.IVariableBinding fieldBinding = fieldAccess.resolveFieldBinding();
|
org.eclipse.jdt.core.dom.IVariableBinding fieldBinding = fieldAccess.resolveFieldBinding();
|
||||||
|
|||||||
@@ -883,4 +883,227 @@ 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 shouldExpandCommandKeyFromQualifiedInheritedStaticMapInSiblingModule(@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.Map;
|
||||||
|
public abstract class BaseRoutes {
|
||||||
|
protected static final Map<String, String> ROUTES = Map.of(
|
||||||
|
"order.pay", "OrderEvent.PAY",
|
||||||
|
"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(BaseRoutes.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExpandCommandKeyFromStaticMethodMapReceiverInSiblingModule(@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/CommandRoutes.java"), """
|
||||||
|
package com.example.routes;
|
||||||
|
import java.util.Map;
|
||||||
|
public class CommandRoutes {
|
||||||
|
public static Map<String, String> routes() {
|
||||||
|
return Map.of(
|
||||||
|
"order.pay", "OrderEvent.PAY",
|
||||||
|
"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.CommandRoutes;
|
||||||
|
public class GenericCommandController {
|
||||||
|
OrderGateway gateway;
|
||||||
|
public void execute(String commandKey) {
|
||||||
|
gateway.trigger(commandKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class OrderGateway {
|
||||||
|
void trigger(String commandKey) {
|
||||||
|
OrderEvent event = OrderEvent.valueOf(CommandRoutes.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExpandRequestParamFromStaticMapLookupInSiblingModule(@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/CommandRoutes.java"), """
|
||||||
|
package com.example.routes;
|
||||||
|
import java.util.Map;
|
||||||
|
public class CommandRoutes {
|
||||||
|
public static final Map<String, String> ROUTES = Map.of(
|
||||||
|
"order.pay", "OrderEvent.PAY",
|
||||||
|
"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.CommandRoutes;
|
||||||
|
public class GenericCommandController {
|
||||||
|
OrderGateway gateway;
|
||||||
|
public void execute(String commandKey) {
|
||||||
|
gateway.trigger(commandKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class OrderGateway {
|
||||||
|
void trigger(String commandKey) {
|
||||||
|
OrderEvent event = OrderEvent.valueOf(CommandRoutes.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")
|
||||||
|
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||||
|
.name("commandKey")
|
||||||
|
.type("String")
|
||||||
|
.annotations(List.of("RequestParam"))
|
||||||
|
.build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<Map<String, String>> variants =
|
||||||
|
EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);
|
||||||
|
|
||||||
|
assertThat(variants).extracting(map -> map.get("commandKey"))
|
||||||
|
.containsExactlyInAnyOrder("order.pay", "order.ship");
|
||||||
|
assertThat(EntryPointBindingExpander.withResolvedPath(entryPoint, variants.get(0)).getName())
|
||||||
|
.isEqualTo("POST /api/commands?commandKey=order.pay");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,28 @@ class CodebaseContextTest {
|
|||||||
assertThat(context.areClassesPolymorphicallyCompatible("a.Dispatcher", "b.Dispatcher")).isFalse();
|
assertThat(context.areClassesPolymorphicallyCompatible("a.Dispatcher", "b.Dispatcher")).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSuperclassFqnShouldPreferImportAwareResolutionWhenBindingMisResolvesPackage() throws IOException {
|
||||||
|
Files.createDirectories(tempDir.resolve("com/example/routes"));
|
||||||
|
Files.writeString(tempDir.resolve("com/example/routes/BaseRoutes.java"), """
|
||||||
|
package com.example.routes;
|
||||||
|
public abstract class BaseRoutes {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("com/example/App.java"), """
|
||||||
|
package com.example;
|
||||||
|
import com.example.routes.BaseRoutes;
|
||||||
|
class OrderGateway extends BaseRoutes {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
assertThat(context.getTypeDeclaration("com.example.routes.BaseRoutes")).isNotNull();
|
||||||
|
assertThat(context.getSuperclassFqn(context.getTypeDeclaration("com.example.OrderGateway")))
|
||||||
|
.isEqualTo("com.example.routes.BaseRoutes");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindClassWithFullyQualifiedAnnotation() throws IOException {
|
void shouldFindClassWithFullyQualifiedAnnotation() throws IOException {
|
||||||
String source = """
|
String source = """
|
||||||
|
|||||||
Reference in New Issue
Block a user