diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java index d2c439d..4d8156a 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpander.java @@ -8,6 +8,8 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import org.eclipse.jdt.core.dom.*; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Expands REST entry points whose {@code @PathVariable} parameters flow into string-keyed @@ -62,6 +64,7 @@ public final class EntryPointBindingExpander { continue; } List> expanded = new ArrayList<>(); + boolean anyCases = false; for (Map base : variants) { List cases = resolveBindingCasesForParameter( entryPoint.getClassName() + "." + entryPoint.getMethodName(), @@ -72,12 +75,19 @@ public final class EntryPointBindingExpander { if (cases.isEmpty()) { continue; } + anyCases = true; for (String caseValue : cases) { Map merged = new LinkedHashMap<>(base); merged.put(parameter.getName(), caseValue); expanded.add(merged); } } + if (!anyCases) { + if (variants.stream().allMatch(Map::isEmpty)) { + return List.of(); + } + continue; + } if (expanded.isEmpty()) { continue; } @@ -429,7 +439,8 @@ public final class EntryPointBindingExpander { return; } for (CallEdge edge : edges) { - if (edge.getTargetMethod() == null || !isEdgeCompatibleWithBindings(edge, partialBindings)) { + if (edge.getTargetMethod() == null + || !isEdgeCompatibleWithBindings(edge, partialBindings, state.methodFqn(), context)) { continue; } List args = edge.getArguments(); @@ -445,12 +456,22 @@ public final class EntryPointBindingExpander { if (!argumentForwardsParameter(args.get(i), state.paramName())) { continue; } + MethodDeclaration targetMethod = findMethodDeclaration(edge.getTargetMethod(), context); + if (targetMethod != null + && isBooleanReturnType(targetMethod) + && !isTrivialSingleArgStringHelper(targetMethod)) { + continue; + } queue.add(new ParameterAliasState(edge.getTargetMethod(), targetParamNames.get(i))); } } } - private static boolean isEdgeCompatibleWithBindings(CallEdge edge, Map partialBindings) { + private static boolean isEdgeCompatibleWithBindings( + CallEdge edge, + Map partialBindings, + String callerMethodFqn, + CodebaseContext context) { if (partialBindings == null || partialBindings.isEmpty()) { return true; } @@ -458,6 +479,7 @@ public final class EntryPointBindingExpander { if (constraint == null || constraint.isBlank()) { return true; } + constraint = inlineBooleanPredicateConstraint(constraint, callerMethodFqn, context); return BooleanConstraintEvaluator.isCompatibleWithBindings(constraint, partialBindings); } @@ -509,7 +531,7 @@ public final class EntryPointBindingExpander { if (!argumentReferencesParameter(argument, paramName, method, context)) { return super.visit(node); } - if (!isValueOfSiteCompatibleWithBindings(node, partialBindings)) { + if (!isValueOfSiteCompatibleWithBindings(node, partialBindings, context)) { return super.visit(node); } String enumType = resolveValueOfEnumType(node, context); @@ -539,7 +561,8 @@ public final class EntryPointBindingExpander { private static boolean isValueOfSiteCompatibleWithBindings( MethodInvocation valueOfInvocation, - Map partialBindings) { + Map partialBindings, + CodebaseContext context) { if (partialBindings == null || partialBindings.isEmpty()) { return true; } @@ -547,6 +570,8 @@ public final class EntryPointBindingExpander { if (branchConstraint == null || branchConstraint.isBlank()) { return true; } + branchConstraint = inlineBooleanPredicateConstraint( + branchConstraint, methodFqnFromNode(valueOfInvocation, context), context); return BooleanConstraintEvaluator.isCompatibleWithBindings(branchConstraint, partialBindings); } @@ -795,6 +820,15 @@ public final class EntryPointBindingExpander { if (selector instanceof SimpleName simpleName) { return paramName.equals(simpleName.getIdentifier()); } + if (selector instanceof MethodInvocation invocation + && invocation.arguments().isEmpty() + && invocation.getExpression() instanceof SimpleName receiver) { + String methodName = invocation.getName().getIdentifier(); + if (("toUpperCase".equals(methodName) || "toLowerCase".equals(methodName) || "trim".equals(methodName)) + && paramName.equals(receiver.getIdentifier())) { + return true; + } + } if (ownerMethod != null && context != null && selector instanceof MethodInvocation invocation) { return isSingleArgHelperForwardingParameter(invocation, paramName, ownerMethod, context); } @@ -811,6 +845,10 @@ public final class EntryPointBindingExpander { while (current != null) { String literal = extractEqualsLiteralForParameter( current.getExpression(), paramName, ownerMethod, context); + if (literal == null) { + literal = extractLiteralFromBooleanPredicateHelper( + current.getExpression(), paramName, ownerMethod, context); + } if (literal != null) { cases.add(literal); } @@ -976,6 +1014,215 @@ public final class EntryPointBindingExpander { return false; } + private static String extractLiteralFromBooleanPredicateHelper( + Expression expression, + String paramName, + MethodDeclaration ownerMethod, + CodebaseContext context) { + if (!(expression instanceof MethodInvocation invocation) || ownerMethod == null || context == null) { + return null; + } + if (invocation.arguments().size() != 1) { + return null; + } + Expression forwarded = (Expression) invocation.arguments().get(0); + if (!parameterExpressionReferencesBinding(forwarded, paramName, ownerMethod, context)) { + return null; + } + MethodDeclaration helper = findHelperMethod(ownerMethod, invocation, context); + if (helper == null || !isBooleanReturnType(helper)) { + return null; + } + return extractSingleEqualsLiteralFromBooleanHelper(helper, context); + } + + private static String extractSingleEqualsLiteralFromBooleanHelper( + MethodDeclaration helper, + CodebaseContext context) { + if (helper.getBody() == null) { + return null; + } + String helperParam = singleParameterName(helper); + if (helperParam == null) { + return null; + } + final int[] returnCount = {0}; + final int[] equalsReturnCount = {0}; + LinkedHashSet literals = new LinkedHashSet<>(); + helper.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + returnCount[0]++; + if (node.getExpression() != null) { + String literal = extractEqualsLiteralForParameter( + node.getExpression(), helperParam, helper, context); + if (literal != null) { + equalsReturnCount[0]++; + literals.add(literal); + } + } + return super.visit(node); + } + }); + if (returnCount[0] == 0 || returnCount[0] != equalsReturnCount[0] || literals.size() != 1) { + return null; + } + return literals.iterator().next(); + } + + static String inlineBooleanPredicateConstraint( + String constraint, + String callerMethodFqn, + CodebaseContext context) { + if (constraint == null || constraint.isBlank() || context == null) { + return constraint; + } + List parts = splitTopLevelAnd(constraint); + if (parts.isEmpty()) { + return constraint; + } + List inlined = new ArrayList<>(); + for (String part : parts) { + String expanded = tryInlineBooleanPredicate(part.trim(), callerMethodFqn, context); + inlined.add(expanded != null ? expanded : part.trim()); + } + return String.join(" && ", inlined); + } + + private static String tryInlineBooleanPredicate( + String constraint, + String callerMethodFqn, + CodebaseContext context) { + if (constraint.startsWith("!(") && constraint.endsWith(")")) { + String inner = constraint.substring(2, constraint.length() - 1).trim(); + String inlinedInner = tryInlineBooleanPredicate(inner, callerMethodFqn, context); + if (inlinedInner != null) { + return "!(" + inlinedInner + ")"; + } + } + Matcher matcher = Pattern.compile( + "^(?:(?[\\w.]+)\\.)?(?\\w+)\\s*\\(\\s*(?[\\w.]+)\\s*\\)$") + .matcher(constraint); + if (!matcher.matches()) { + return null; + } + MethodDeclaration caller = findMethodDeclaration(callerMethodFqn, context); + if (caller == null) { + return null; + } + String qualifier = matcher.group("qual"); + String helperName = matcher.group("method"); + String argumentName = matcher.group("arg"); + MethodDeclaration helper = resolveBooleanPredicateHelper(caller, qualifier, helperName, context); + if (helper == null) { + return null; + } + String helperParam = singleParameterName(helper); + if (helperParam == null) { + return null; + } + LinkedHashSet returnExpressions = new LinkedHashSet<>(); + helper.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + if (node.getExpression() != null) { + returnExpressions.add(node.getExpression().toString()); + } + return super.visit(node); + } + }); + if (returnExpressions.size() != 1) { + return null; + } + String literal = extractSingleEqualsLiteralFromBooleanHelper(helper, context); + if (literal == null) { + return null; + } + return returnExpressions.iterator().next() + .replaceAll("\\b" + Pattern.quote(helperParam) + "\\b", argumentName); + } + + private static MethodDeclaration resolveBooleanPredicateHelper( + MethodDeclaration caller, + String qualifier, + String helperName, + CodebaseContext context) { + if (qualifier != null && !qualifier.isBlank()) { + MethodDeclaration helper = findHelperOnType(qualifier, helperName, context); + if (helper != null) { + return helper; + } + CompilationUnit compilationUnit = caller.getRoot() instanceof CompilationUnit cu ? cu : null; + TypeDeclaration typeDeclaration = context.getTypeDeclaration(qualifier, compilationUnit); + if (typeDeclaration != null) { + helper = context.findMethodDeclaration(typeDeclaration, helperName, true); + if (helper != null) { + return helper; + } + return findHelperOnType(context.getFqn(typeDeclaration), helperName, context); + } + return null; + } + MethodDeclaration helper = findInstanceHelperOnOwnerType(caller, helperName, context); + if (helper != null) { + return helper; + } + CompilationUnit compilationUnit = caller.getRoot() instanceof CompilationUnit cu ? cu : null; + return findStaticImportHelper(helperName, compilationUnit, context); + } + + private static List splitTopLevelAnd(String constraint) { + List parts = new ArrayList<>(); + int depth = 0; + int start = 0; + for (int i = 0; i < constraint.length() - 1; i++) { + char c = constraint.charAt(i); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + } else if (depth == 0 && c == '&' && constraint.charAt(i + 1) == '&') { + parts.add(constraint.substring(start, i)); + start = i + 2; + i++; + } + } + parts.add(constraint.substring(start)); + return parts; + } + + private static String methodFqnFromNode(ASTNode node, CodebaseContext context) { + MethodDeclaration method = AstUtils.findEnclosingMethod(node); + TypeDeclaration type = AstUtils.findEnclosingType(node); + if (method == null || type == null || context == null) { + return null; + } + return context.getFqn(type) + "." + method.getName().getIdentifier(); + } + + private static String singleParameterName(MethodDeclaration method) { + if (method.parameters().size() != 1) { + return null; + } + Object paramObj = method.parameters().get(0); + if (paramObj instanceof SingleVariableDeclaration param) { + return param.getName().getIdentifier(); + } + return null; + } + + private static boolean isBooleanReturnType(MethodDeclaration method) { + Type returnType = method.getReturnType2(); + if (returnType instanceof PrimitiveType primitiveType) { + return primitiveType.getPrimitiveTypeCode() == PrimitiveType.BOOLEAN; + } + if (returnType instanceof SimpleType simpleType) { + String name = simpleType.getName().getFullyQualifiedName(); + return "boolean".equals(name) || "Boolean".equals(name); + } + return false; + } + private static MethodDeclaration findMethodDeclaration(String methodFqn, CodebaseContext context) { if (methodFqn == null || !methodFqn.contains(".")) { return null; diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpanderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpanderTest.java index a565ec3..1a383a7 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpanderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EntryPointBindingExpanderTest.java @@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.service; import click.kamil.springstatemachineexporter.analysis.model.CallChain; import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -1091,4 +1092,346 @@ class EntryPointBindingExpanderTest { assertThat(variants.stream().map(v -> v.get("machineType")).distinct()) .containsExactlyInAnyOrder("ORDER", "DOCUMENT"); } + + @Test + void shouldExtractMachineTypeCasesFromBooleanPredicateIfChain(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + class StateMachineDispatcher { + void dispatch(String machineType, String eventString) { + if (matchesOrder(machineType)) { + OrderEvent.valueOf(eventString.toUpperCase()); + } else if (matchesDocument(machineType)) { + DocumentEvent.valueOf(eventString.toUpperCase()); + } + } + boolean matchesOrder(String raw) { + return "ORDER".equalsIgnoreCase(raw.trim()); + } + boolean matchesDocument(String raw) { + return "DOCUMENT".equalsIgnoreCase(raw.trim()); + } + } + 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); + + List cases = EntryPointBindingExpander.extractStringSwitchCases( + "com.example.StateMachineDispatcher.dispatch", "machineType", context); + + assertThat(cases).containsExactlyInAnyOrder("ORDER", "DOCUMENT"); + } + + @Test + void shouldInlineBooleanPredicateEdgeConstraints(@TempDir Path tempDir) throws Exception { + String source = """ + package com.example; + class StateMachineDispatcher { + void dispatch(String machineType, String eventString) { + if (matchesOrder(machineType)) { + fireOrder(eventString); + } else if (matchesDocument(machineType)) { + fireDocument(eventString); + } + } + boolean matchesOrder(String raw) { + return "ORDER".equalsIgnoreCase(raw.trim()); + } + boolean matchesDocument(String raw) { + return "DOCUMENT".equalsIgnoreCase(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); + + String caller = "com.example.StateMachineDispatcher.dispatch"; + String orderConstraint = EntryPointBindingExpander.inlineBooleanPredicateConstraint( + "matchesOrder(machineType)", caller, context); + String documentConstraint = EntryPointBindingExpander.inlineBooleanPredicateConstraint( + "matchesDocument(machineType)", caller, context); + + assertThat(orderConstraint).contains("ORDER"); + assertThat(documentConstraint).contains("DOCUMENT"); + assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings( + orderConstraint, Map.of("machineType", "DOCUMENT"))).isFalse(); + assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings( + documentConstraint, Map.of("machineType", "DOCUMENT"))).isTrue(); + + String elseIfConstraint = EntryPointBindingExpander.inlineBooleanPredicateConstraint( + "!(matchesOrder(machineType)) && matchesDocument(machineType)", caller, context); + assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings( + elseIfConstraint, Map.of("machineType", "DOCUMENT"))).isTrue(); + assertThat(BooleanConstraintEvaluator.isCompatibleWithBindings( + elseIfConstraint, Map.of("machineType", "ORDER"))).isFalse(); + } + + @Test + void shouldFilterDelegatedBranchesUsingBooleanPredicateConstraints(@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 (matchesOrder(machineType)) { + fireOrder(eventString); + } else if (matchesDocument(machineType)) { + fireDocument(eventString); + } + } + boolean matchesOrder(String raw) { + return "ORDER".equalsIgnoreCase(raw.trim()); + } + boolean matchesDocument(String raw) { + return "DOCUMENT".equalsIgnoreCase(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> graph = engine.buildCallGraph(); + + List machineTypes = EntryPointBindingExpander.expandPathVariableBindings( + 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())) + .build(), + context, + graph).stream().map(v -> v.get("machineType")).toList(); + + assertThat(machineTypes).containsExactlyInAnyOrder("ORDER", "DOCUMENT"); + } + + @Test + void shouldExpandMachineTypeFromBooleanPredicateHelper(@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 (matchesOrder(machineType)) { + fireOrder(eventString); + } else if (matchesDocument(machineType)) { + fireDocument(eventString); + } + } + boolean matchesOrder(String raw) { + return "ORDER".equalsIgnoreCase(raw.trim()); + } + boolean matchesDocument(String raw) { + return "DOCUMENT".equalsIgnoreCase(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> 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> 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"); + } + + @Test + void shouldExpandMachineTypeFromCrossClassBooleanPredicateHelper(@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 (TypeMatcher.matchesOrder(machineType)) { + OrderEvent.valueOf(eventString.toUpperCase()); + } else if (TypeMatcher.matchesDocument(machineType)) { + DocumentEvent.valueOf(eventString.toUpperCase()); + } + } + } + class TypeMatcher { + static boolean matchesOrder(String raw) { + return "ORDER".equalsIgnoreCase(raw.trim()); + } + static boolean matchesDocument(String raw) { + return "DOCUMENT".equalsIgnoreCase(raw.trim()); + } + } + 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> 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> variants = + EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph); + + assertThat(variants.stream().map(v -> v.get("machineType")).distinct()) + .containsExactlyInAnyOrder("ORDER", "DOCUMENT"); + } + + @Test + void shouldNotExpandWhenBooleanPredicateMatchesMultipleLiterals(@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 (matchesAny(machineType)) { + OrderEvent.valueOf(eventString.toUpperCase()); + } + } + boolean matchesAny(String raw) { + if ("ORDER".equalsIgnoreCase(raw)) { + return true; + } + return "DOCUMENT".equalsIgnoreCase(raw); + } + } + 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> 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> variants = + EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph); + + assertThat(variants).isEmpty(); + } }