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:
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user