Expand route maps from static factory methods with single Map.of return.

Follow one-return static factories that directly yield compile-time map literals for field initializers, static-block assignments, and putAll sources while failing closed on runtime-built maps or multiple returns.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 18:51:59 +02:00
parent a675a773da
commit bb28e72bbe
2 changed files with 357 additions and 0 deletions

View File

@@ -638,6 +638,13 @@ public final class EntryPointBindingExpander {
if (direct != null) { if (direct != null) {
return direct; return direct;
} }
if (receiver instanceof MethodInvocation methodCall) {
Map<String, String> factory = extractCompileTimeMapFromStaticMethodCall(
methodCall, literalDeclaringType, compilationUnit, context);
if (factory != null) {
return factory;
}
}
String fieldName = null; String fieldName = null;
TypeDeclaration declaringType = null; TypeDeclaration declaringType = null;
if (receiver instanceof FieldAccess fieldAccess) { if (receiver instanceof FieldAccess fieldAccess) {
@@ -712,6 +719,12 @@ public final class EntryPointBindingExpander {
if (doubleBrace != null) { if (doubleBrace != null) {
return doubleBrace; return doubleBrace;
} }
CompilationUnit compilationUnit = typeDecl.getRoot() instanceof CompilationUnit cu ? cu : null;
Map<String, String> factory = extractCompileTimeMapFromStaticMethodCall(
initializer, typeDecl, compilationUnit, context);
if (factory != null) {
return factory;
}
} }
return extractStaticBlockMapPutEntries(typeDecl, fieldName, context); return extractStaticBlockMapPutEntries(typeDecl, fieldName, context);
} }
@@ -773,6 +786,14 @@ public final class EntryPointBindingExpander {
foundPut[0] = true; foundPut[0] = true;
return super.visit(node); return super.visit(node);
} }
CompilationUnit compilationUnit = block.getRoot() instanceof CompilationUnit cu ? cu : null;
Map<String, String> factory = extractCompileTimeMapFromStaticMethodCall(
right, typeDecl, compilationUnit, context);
if (factory != null) {
entries.putAll(factory);
foundPut[0] = true;
return super.visit(node);
}
if (right instanceof SimpleName localName if (right instanceof SimpleName localName
&& mapBuilderLocals.contains(localName.getIdentifier())) { && mapBuilderLocals.contains(localName.getIdentifier())) {
linkedLocals.add(localName.getIdentifier()); linkedLocals.add(localName.getIdentifier());
@@ -945,6 +966,65 @@ public final class EntryPointBindingExpander {
return false; return false;
} }
private static Map<String, String> extractCompileTimeMapFromStaticMethodCall(
Expression expression,
TypeDeclaration defaultDeclaringType,
CompilationUnit compilationUnit,
CodebaseContext context) {
if (!(expression instanceof MethodInvocation invocation)) {
return null;
}
org.eclipse.jdt.core.dom.IMethodBinding binding = invocation.resolveMethodBinding();
if (binding == null || !Modifier.isStatic(binding.getModifiers())) {
return null;
}
TypeDeclaration declaringType = null;
if (binding.getDeclaringClass() != null) {
declaringType = context.getTypeDeclaration(binding.getDeclaringClass().getQualifiedName());
}
Expression receiver = invocation.getExpression();
if (declaringType == null && receiver != null) {
declaringType = resolveHelperDeclaringType(receiver, compilationUnit, context);
}
if (declaringType == null) {
declaringType = defaultDeclaringType;
}
if (declaringType == null) {
return null;
}
MethodDeclaration method = context.findMethodDeclaration(
declaringType, invocation.getName().getIdentifier(), true);
if (method == null || method.getBody() == null) {
return null;
}
Expression returnExpression = extractSingleReturnExpression(method.getBody());
if (returnExpression == null) {
return null;
}
Map<String, String> direct = extractMapOfLiteralEntries(returnExpression, declaringType, context);
if (direct != null) {
return direct;
}
return extractDoubleBraceMapPutEntries(returnExpression, declaringType, context);
}
private static Expression extractSingleReturnExpression(Block body) {
Expression[] found = {null};
boolean[] multiple = {false};
body.accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
if (found[0] != null) {
multiple[0] = true;
} else {
found[0] = node.getExpression();
}
return super.visit(node);
}
});
return multiple[0] ? null : found[0];
}
private static Map<String, String> extractMapOfLiteralEntries( private static Map<String, String> extractMapOfLiteralEntries(
Expression expression, Expression expression,
TypeDeclaration declaringType, TypeDeclaration declaringType,

View File

@@ -4044,4 +4044,281 @@ class EntryPointBindingExpanderTest {
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 shouldExpandCommandKeyFromStaticFactoryMethodReturningMapOf(@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);
}
}
class OrderGateway {
private static final Map<String, OrderEvent> ROUTES = routes();
static Map<String, OrderEvent> routes() {
return Map.of(
"order.pay", OrderEvent.PAY,
"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<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");
}
@Test
void shouldExpandCommandKeyFromCrossClassStaticFactoryMethodReturningMapOf(@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);
}
}
class OrderGateway {
private static final Map<String, OrderEvent> ROUTES = CommandRoutes.seed();
void trigger(String commandKey) {
StateMachine sm = new StateMachine();
sm.sendEvent(ROUTES.get(commandKey));
}
}
class CommandRoutes {
static Map<String, OrderEvent> seed() {
return 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<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");
}
@Test
void shouldExpandCommandKeyFromStaticBlockStaticFactoryAssignment(@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);
}
}
class OrderGateway {
private static final Map<String, OrderEvent> ROUTES;
static {
ROUTES = routes();
}
static Map<String, OrderEvent> routes() {
return Map.of(
"order.pay", OrderEvent.PAY,
"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<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");
}
@Test
void shouldNotExpandCommandKeyFromStaticFactoryBuildingMapAtRuntime(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import java.util.HashMap;
import java.util.Map;
public class GenericCommandController {
OrderGateway gateway;
public void execute(String commandKey) {
gateway.trigger(commandKey);
}
}
class OrderGateway {
private static final Map<String, OrderEvent> ROUTES = routes();
static Map<String, OrderEvent> routes() {
Map<String, OrderEvent> built = new HashMap<>();
built.put("order.pay", OrderEvent.PAY);
built.put("order.ship", OrderEvent.SHIP);
return built;
}
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<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).isEmpty();
}
@Test
void shouldNotExpandCommandKeyFromStaticFactoryWithMultipleReturns(@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);
}
}
class OrderGateway {
private static final Map<String, OrderEvent> ROUTES = routes(true);
static Map<String, OrderEvent> routes(boolean full) {
if (full) {
return Map.of(
"order.pay", OrderEvent.PAY,
"order.ship", OrderEvent.SHIP
);
}
return Map.of("order.pay", OrderEvent.PAY);
}
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<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).isEmpty();
}
} }