Resolve path bindings through single-arg string helper methods.

Enum.valueOf(normalize(event)) now expands when normalize trivially forwards its parameter; add expander regression test and assert flow highlight helper in enterprise HTML export.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 17:46:09 +02:00
parent 5348dafb69
commit 8ef6e5ef8f
3 changed files with 122 additions and 7 deletions

View File

@@ -506,7 +506,7 @@ public final class EntryPointBindingExpander {
return super.visit(node);
}
Expression argument = (Expression) node.arguments().get(0);
if (!argumentReferencesParameter(argument, paramName)) {
if (!argumentReferencesParameter(argument, paramName, method, context)) {
return super.visit(node);
}
if (!isValueOfSiteCompatibleWithBindings(node, partialBindings)) {
@@ -551,24 +551,91 @@ public final class EntryPointBindingExpander {
}
private static boolean argumentReferencesParameter(Expression argument, String paramName) {
return argumentReferencesParameter(argument, paramName, null, null);
}
private static boolean argumentReferencesParameter(
Expression argument,
String paramName,
MethodDeclaration ownerMethod,
CodebaseContext context) {
if (argument instanceof SimpleName simpleName) {
return paramName.equals(simpleName.getIdentifier());
}
if (argument instanceof MethodInvocation invocation
&& "toUpperCase".equals(invocation.getName().getIdentifier())
&& invocation.arguments().isEmpty()
&& invocation.getExpression() instanceof SimpleName simpleName) {
return paramName.equals(simpleName.getIdentifier());
String methodName = invocation.getName().getIdentifier();
if (("toUpperCase".equals(methodName) || "toLowerCase".equals(methodName) || "trim".equals(methodName))
&& paramName.equals(simpleName.getIdentifier())) {
return true;
}
}
if (argument instanceof MethodInvocation invocation
&& "toLowerCase".equals(invocation.getName().getIdentifier())
&& invocation.arguments().isEmpty()
&& invocation.getExpression() instanceof SimpleName simpleName) {
return paramName.equals(simpleName.getIdentifier());
&& ownerMethod != null
&& context != null
&& isSingleArgHelperForwardingParameter(invocation, paramName, ownerMethod, context)) {
return true;
}
return false;
}
private static boolean isSingleArgHelperForwardingParameter(
MethodInvocation helperInvocation,
String paramName,
MethodDeclaration ownerMethod,
CodebaseContext context) {
if (helperInvocation.arguments().size() != 1) {
return false;
}
Expression forwarded = (Expression) helperInvocation.arguments().get(0);
if (!argumentReferencesParameter(forwarded, paramName, ownerMethod, context)) {
return false;
}
MethodDeclaration helper = findHelperMethod(ownerMethod, helperInvocation, context);
return helper != null && isTrivialSingleArgStringHelper(helper);
}
private static MethodDeclaration findHelperMethod(
MethodDeclaration ownerMethod,
MethodInvocation helperInvocation,
CodebaseContext context) {
ASTNode parent = ownerMethod.getParent();
if (!(parent instanceof TypeDeclaration typeDeclaration)) {
return null;
}
String helperName = helperInvocation.getName().getIdentifier();
return context.findMethodDeclaration(typeDeclaration, helperName, true);
}
private static boolean isTrivialSingleArgStringHelper(MethodDeclaration helper) {
if (helper.parameters().size() != 1 || helper.getBody() == null) {
return false;
}
String helperParam = null;
Object paramObj = helper.parameters().get(0);
if (paramObj instanceof SingleVariableDeclaration param) {
helperParam = param.getName().getIdentifier();
}
if (helperParam == null) {
return false;
}
Set<String> known = new LinkedHashSet<>();
known.add(helperParam);
known.addAll(collectIntraMethodParameterAliases(helper, helperParam));
final boolean[] returnsKnownBinding = {false};
helper.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(ReturnStatement node) {
if (expressionReferencesKnownBinding(node.getExpression(), known)) {
returnsKnownBinding[0] = true;
}
return super.visit(node);
}
});
return returnsKnownBinding[0];
}
private static String resolveValueOfEnumType(MethodInvocation valueOfInvocation, CodebaseContext context) {
Expression receiver = valueOfInvocation.getExpression();
if (receiver != null) {

View File

@@ -532,4 +532,51 @@ class EntryPointBindingExpanderTest {
assertThat(variants).extracting(map -> map.get("event"))
.containsExactlyInAnyOrder("PAY", "SHIP");
}
@Test
void shouldExpandEventWhenParameterIsPassedThroughSingleArgHelper(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
public class OrderController {
OrderGateway gateway;
public void transition(String event) {
gateway.trigger(event);
}
}
class OrderGateway {
void trigger(String event) {
OrderEvent.valueOf(normalize(event));
}
String normalize(String raw) {
return raw.toUpperCase();
}
}
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.OrderController")
.methodName("transition")
.name("POST /api/order/{event}")
.parameters(List.of(EntryPoint.Parameter.builder()
.name("event")
.type("String")
.annotations(List.of("PathVariable"))
.build()))
.build();
List<Map<String, String>> variants =
EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph);
assertThat(variants).extracting(map -> map.get("event"))
.containsExactlyInAnyOrder("PAY", "SHIP");
}
}