Expand path bindings for if-equals on helper-wrapped parameters.

Use balanced-paren receiver-equals substitution so compound branch constraints stay correct while supporting forms like "ORDER".equalsIgnoreCase(normalizeType(machineType)).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 18:03:40 +02:00
parent 2d478d5779
commit 8d7e04cc33
5 changed files with 262 additions and 15 deletions

View File

@@ -49,4 +49,17 @@ class BooleanConstraintEvaluatorBindingsTest {
"command == ORDER_PAY",
Map.of("commandKey", "order.pay"))).isFalse();
}
@Test
void shouldEvaluateEqualsIgnoreCaseWithHelperWrappedParameter() {
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
"\"ORDER\".equalsIgnoreCase(normalizeType(machineType))",
Map.of("machineType", "ORDER"))).isTrue();
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
"\"ORDER\".equalsIgnoreCase(normalizeType(machineType))",
Map.of("machineType", "DOCUMENT"))).isFalse();
assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings(
"\"DOCUMENT\".equalsIgnoreCase(normalizeType(machineType))",
Map.of("machineType", "DOCUMENT"))).isTrue();
}
}

View File

@@ -889,4 +889,78 @@ class EntryPointBindingExpanderTest {
assertThat(variants).extracting(map -> map.get("event"))
.containsExactlyInAnyOrder("PAY", "SHIP");
}
@Test
void shouldExpandMachineTypeFromIfEqualsOnSingleArgHelper(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
public class MachineController {
StateMachineDispatcher dispatcher;
public void transition(String machineType, String event) {
dispatcher.dispatch(machineType, event);
}
}
class StateMachineDispatcher {
void dispatch(String machineType, String eventString) {
if ("ORDER".equalsIgnoreCase(normalizeType(machineType))) {
fireOrder(eventString);
} else if ("DOCUMENT".equalsIgnoreCase(normalizeType(machineType))) {
fireDocument(eventString);
}
}
String normalizeType(String raw) {
return raw.trim();
}
void fireOrder(String eventString) {
OrderEvent.valueOf(eventString.toUpperCase());
}
void fireDocument(String eventString) {
DocumentEvent.valueOf(eventString.toUpperCase());
}
}
enum OrderEvent { PAY, SHIP }
enum DocumentEvent { SUBMIT, APPROVE }
""";
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.MachineController")
.methodName("transition")
.name("POST /api/machine/{machineType}/transition/{event}")
.parameters(List.of(
EntryPoint.Parameter.builder()
.name("machineType")
.type("String")
.annotations(List.of("PathVariable"))
.build(),
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.stream().map(v -> v.get("machineType")).distinct())
.containsExactlyInAnyOrder("ORDER", "DOCUMENT");
assertThat(variants.stream()
.filter(v -> "ORDER".equals(v.get("machineType")))
.map(v -> v.get("event"))
.toList())
.containsExactlyInAnyOrder("PAY", "SHIP");
assertThat(variants.stream()
.filter(v -> "DOCUMENT".equals(v.get("machineType")))
.map(v -> v.get("event"))
.toList())
.containsExactlyInAnyOrder("SUBMIT", "APPROVE");
}
}

View File

@@ -17,7 +17,7 @@ import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Path binding expansion must resolve helpers declared in sibling Gradle modules without executing the build.
* Path binding expansion must resolve helpers declared in sibling modules without executing the build.
*/
class MultiModulePathBindingExpanderTest {
@@ -94,4 +94,90 @@ class MultiModulePathBindingExpanderTest {
entryPoint, Map.of("event", "PAY")).getName())
.isEqualTo("POST /api/order/PAY");
}
@Test
void shouldExpandEventUsingHelperFromSiblingMavenModule(@TempDir Path tempDir) throws Exception {
Path root = tempDir.resolve("order-maven");
Files.createDirectories(root.resolve(".git"));
Files.writeString(root.resolve("pom.xml"), """
<project><groupId>com.example</groupId><artifactId>order-maven</artifactId></project>
""");
Path apiModule = root.resolve("api-module");
Files.createDirectories(apiModule.resolve("src/main/java/com/example/util"));
Files.writeString(apiModule.resolve("pom.xml"), """
<project><groupId>com.example</groupId><artifactId>api-module</artifactId></project>
""");
Files.writeString(apiModule.resolve("src/main/java/com/example/util/EventNormalizer.java"), """
package com.example.util;
public class EventNormalizer {
public static String normalize(String raw) {
return raw.toUpperCase();
}
}
""");
Path coreModule = root.resolve("core-module");
Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
Files.writeString(coreModule.resolve("pom.xml"), """
<project>
<groupId>com.example</groupId>
<artifactId>core-module</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>api-module</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
""");
Files.writeString(coreModule.resolve("src/main/java/com/example/OrderGateway.java"), """
package com.example;
import com.example.util.EventNormalizer;
public class OrderController {
OrderGateway gateway;
public void transition(String event) {
gateway.trigger(event);
}
}
class OrderGateway {
void trigger(String event) {
OrderEvent.valueOf(EventNormalizer.normalize(event));
}
}
enum OrderEvent { PAY, SHIP }
""");
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());
assertThat(context.getTypeDeclaration("com.example.util.EventNormalizer")).isNotNull();
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
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, callGraph);
assertThat(variants).extracting(map -> map.get("event"))
.containsExactlyInAnyOrder("PAY", "SHIP");
}
}