Skip to content

Commit 61f1384

Browse files
jnthntatumcopybara-github
authored andcommitted
Add comprehension nesting validator.
PiperOrigin-RevId: 814924866
1 parent ac7c96b commit 61f1384

File tree

5 files changed

+286
-0
lines changed

5 files changed

+286
-0
lines changed

validator/src/main/java/dev/cel/validator/validators/BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ java_library(
8484
],
8585
)
8686

87+
java_library(
88+
name = "comprehension_nesting_limit_validator",
89+
srcs = [
90+
"ComprehensionNestingLimitValidator.java",
91+
],
92+
tags = [
93+
],
94+
deps = [
95+
"//bundle:cel",
96+
"//common/ast",
97+
"//common/navigation",
98+
"//common/navigation:common",
99+
"//validator:ast_validator",
100+
"@maven//:com_google_guava_guava",
101+
],
102+
)
103+
87104
java_library(
88105
name = "literal_validator",
89106
srcs = [
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.validator.validators;
16+
17+
import static com.google.common.base.Preconditions.checkArgument;
18+
19+
import dev.cel.bundle.Cel;
20+
import dev.cel.common.ast.CelExpr.ExprKind;
21+
import dev.cel.common.navigation.CelNavigableAst;
22+
import dev.cel.common.navigation.CelNavigableExpr;
23+
import dev.cel.common.navigation.TraversalOrder;
24+
import dev.cel.validator.CelAstValidator;
25+
26+
/**
27+
* Checks that the nesting depth of comprehensions does not exceed the configured limit. Nesting
28+
* occurs when a comprehension is used in the range expression or the body of a comprehension.
29+
*
30+
* <p>Trivial comprehensions (comprehensions over an empty range) do not count towards the limit.
31+
*/
32+
public final class ComprehensionNestingLimitValidator implements CelAstValidator {
33+
private final int nestingLimit;
34+
35+
/**
36+
* Constructs a new instance of {@link ComprehensionNestingLimitValidator} with the configured
37+
* maxNesting as its limit. A limit of 0 means no comprehensions are allowed.
38+
*/
39+
public static ComprehensionNestingLimitValidator newInstance(int maxNesting) {
40+
checkArgument(maxNesting >= 0);
41+
return new ComprehensionNestingLimitValidator(maxNesting);
42+
}
43+
44+
@Override
45+
public void validate(CelNavigableAst navigableAst, Cel cel, IssuesFactory issuesFactory) {
46+
navigableAst
47+
.getRoot()
48+
.allNodes(TraversalOrder.PRE_ORDER)
49+
.filter(node -> node.getKind() == ExprKind.Kind.COMPREHENSION)
50+
.filter(node -> nestingLevel(node) > nestingLimit)
51+
.forEach(
52+
node ->
53+
issuesFactory.addError(
54+
node.id(),
55+
String.format(
56+
"comprehension nesting exceeds the configured limit: %s.", nestingLimit)));
57+
}
58+
59+
private static boolean isTrivialComprehension(CelNavigableExpr node) {
60+
return (node.expr().comprehension().iterRange().getKind() == ExprKind.Kind.LIST
61+
&& node.expr().comprehension().iterRange().list().elements().isEmpty())
62+
|| (node.expr().comprehension().iterRange().getKind() == ExprKind.Kind.MAP
63+
&& node.expr().comprehension().iterRange().map().entries().isEmpty());
64+
}
65+
66+
private static int nestingLevel(CelNavigableExpr node) {
67+
if (isTrivialComprehension(node)) {
68+
return 0;
69+
}
70+
int count = 1;
71+
while (node.parent().isPresent()) {
72+
CelNavigableExpr parent = node.parent().get();
73+
74+
if (parent.getKind() == ExprKind.Kind.COMPREHENSION && !isTrivialComprehension(parent)) {
75+
count += 1;
76+
}
77+
node = parent;
78+
}
79+
return count;
80+
}
81+
82+
private ComprehensionNestingLimitValidator(int maxNesting) {
83+
this.nestingLimit = maxNesting;
84+
}
85+
}

validator/src/test/java/dev/cel/validator/validators/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ java_library(
1616
"//common:proto_ast",
1717
"//common/internal:proto_time_utils",
1818
"//common/types",
19+
"//extensions",
1920
"//extensions:optional_library",
21+
"//parser:macro",
2022
"//runtime",
2123
"//runtime:function_binding",
2224
"//validator",
2325
"//validator:validator_builder",
2426
"//validator/validators:ast_depth_limit_validator",
27+
"//validator/validators:comprehension_nesting_limit_validator",
2528
"//validator/validators:duration",
2629
"//validator/validators:homogeneous_literal",
2730
"//validator/validators:regex",
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.validator.validators;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static org.junit.Assert.assertThrows;
19+
20+
import com.google.testing.junit.testparameterinjector.TestParameter;
21+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
22+
import dev.cel.bundle.Cel;
23+
import dev.cel.bundle.CelFactory;
24+
import dev.cel.common.CelAbstractSyntaxTree;
25+
import dev.cel.common.CelIssue.Severity;
26+
import dev.cel.common.CelValidationException;
27+
import dev.cel.common.CelValidationResult;
28+
import dev.cel.common.types.SimpleType;
29+
import dev.cel.extensions.CelExtensions;
30+
import dev.cel.parser.CelStandardMacro;
31+
import dev.cel.validator.CelValidator;
32+
import dev.cel.validator.CelValidatorFactory;
33+
import org.junit.Test;
34+
import org.junit.runner.RunWith;
35+
36+
@RunWith(TestParameterInjector.class)
37+
public class ComprehensionNestingLimitValidatorTest {
38+
39+
private static final Cel CEL =
40+
CelFactory.standardCelBuilder()
41+
.addCompilerLibraries(
42+
CelExtensions.optional(), CelExtensions.comprehensions(), CelExtensions.bindings())
43+
.addRuntimeLibraries(CelExtensions.optional(), CelExtensions.comprehensions())
44+
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
45+
.addVar("x", SimpleType.DYN)
46+
.build();
47+
48+
private static final CelValidator CEL_VALIDATOR =
49+
CelValidatorFactory.standardCelValidatorBuilder(CEL)
50+
.addAstValidators(ComprehensionNestingLimitValidator.newInstance(1))
51+
.build();
52+
53+
@Test
54+
public void comprehensionNestingLimit_populatesErrors(
55+
@TestParameter({
56+
"[1, 2, 3].map(x, [1, 2, 3].map(y, x + y))",
57+
"[1, 2, 3].map(x, [1, 2, 3].map(y, x + y).size() > 0, x)",
58+
"[1, 2, 3].all(x, [1, 2, 3].exists(y, x + y > 0))",
59+
"[1, 2, 3].map(x, {x: [1, 2, 3].map(y, x + y)})",
60+
"[1, 2, 3].exists(i, v, i < 3 && [1, 2, 3].all(j, v2, j < 3 && v2 > 0))",
61+
"{1: 2}.all(k, {2: 3}.all(k2, k != k2))"
62+
})
63+
String expr)
64+
throws Exception {
65+
CelAbstractSyntaxTree ast = CEL.compile(expr).getAst();
66+
67+
CelValidationResult result = CEL_VALIDATOR.validate(ast);
68+
69+
assertThat(result.hasError()).isTrue();
70+
assertThat(result.getAllIssues()).hasSize(1);
71+
assertThat(result.getAllIssues().get(0).getSeverity()).isEqualTo(Severity.ERROR);
72+
assertThat(result.getAllIssues().get(0).toDisplayString(ast.getSource()))
73+
.contains("comprehension nesting exceeds the configured limit: 1.");
74+
}
75+
76+
@Test
77+
public void comprehensionNestingLimit_accumulatesErrors(
78+
@TestParameter({
79+
"[1, 2, 3].map(x, [1, 2, 3].map(y, [1, 2, 3].map(z, x + y + z)))",
80+
})
81+
String expr)
82+
throws Exception {
83+
CelAbstractSyntaxTree ast = CEL.compile(expr).getAst();
84+
85+
CelValidationResult result = CEL_VALIDATOR.validate(ast);
86+
87+
assertThat(result.hasError()).isTrue();
88+
assertThat(result.getAllIssues()).hasSize(2);
89+
}
90+
91+
@Test
92+
public void comprehensionNestingLimit_limitConfigurable(
93+
@TestParameter({
94+
"[1, 2, 3].map(x, [1, 2, 3].map(y, x + y))",
95+
"[1, 2, 3].map(x, [1, 2, 3].map(y, x + y).size() > 0, x)",
96+
"[1, 2, 3].all(x, [1, 2, 3].exists(y, x + y > 0))",
97+
"[1, 2, 3].map(x, {x: [1, 2, 3].map(y, x + y)})",
98+
"[1, 2, 3].exists(i, v, i < 3 && [1, 2, 3].all(j, v2, j < 3 && v2 > 0))",
99+
"{1: 2}.all(k, {2: 3}.all(k2, k != k2))"
100+
})
101+
String expr)
102+
throws Exception {
103+
CelAbstractSyntaxTree ast = CEL.compile(expr).getAst();
104+
CelValidator celValidator =
105+
CelValidatorFactory.standardCelValidatorBuilder(CEL)
106+
.addAstValidators(ComprehensionNestingLimitValidator.newInstance(2))
107+
.build();
108+
109+
CelValidationResult result = celValidator.validate(ast);
110+
111+
assertThat(result.hasError()).isFalse();
112+
}
113+
114+
@Test
115+
public void comprehensionNestingLimit_trivialLoopsDontCount(
116+
@TestParameter({
117+
"cel.bind(x, [1, 2].map(x, x + 1), x + [1, 2].map(x, x + 1))",
118+
"optional.of(1).optMap(x, [1, 2, 3].exists(y, y == x))",
119+
"[].map(x, [1, 2, 3].map(y, x + y))",
120+
"{}.map(k1, {1: 2, 3: 4}.map(k2, k1 + k2))",
121+
"[1, 2, 3].map(x, cel.bind(y, 2, x + y))",
122+
"[1, 2, 3].map(x, optional.of(1).optMap(y, x + y).orValue(0))",
123+
})
124+
String expr)
125+
throws Exception {
126+
CelAbstractSyntaxTree ast = CEL.compile(expr).getAst();
127+
128+
CelValidationResult result = CEL_VALIDATOR.validate(ast);
129+
130+
assertThat(result.hasError()).isFalse();
131+
}
132+
133+
@Test
134+
public void comprehensionNestingLimit_zeroLimitAcceptedComprehenions(
135+
@TestParameter({
136+
"cel.bind(x, 1, x + 1)",
137+
"optional.of(1).optMap(x, x + 1)",
138+
"[].map(x, int(x))",
139+
"cel.bind(x, 1 + [].map(x, int(x)).size(), x + 1)"
140+
})
141+
String expr)
142+
throws Exception {
143+
CelAbstractSyntaxTree ast = CEL.compile(expr).getAst();
144+
145+
CelValidator celValidator =
146+
CelValidatorFactory.standardCelValidatorBuilder(CEL)
147+
.addAstValidators(ComprehensionNestingLimitValidator.newInstance(0))
148+
.build();
149+
150+
CelValidationResult result = celValidator.validate(ast);
151+
152+
assertThat(result.hasError()).isFalse();
153+
}
154+
155+
@Test
156+
public void comprehensionNestingLimit_zeroLimitRejectedComprehensions(
157+
@TestParameter({
158+
"[1].map(x, x)",
159+
"[1].exists(x, x > 0)",
160+
"[].exists(x, [1].all(y, y > 0))",
161+
})
162+
String expr)
163+
throws Exception {
164+
CelAbstractSyntaxTree ast = CEL.compile(expr).getAst();
165+
166+
CelValidator celValidator =
167+
CelValidatorFactory.standardCelValidatorBuilder(CEL)
168+
.addAstValidators(ComprehensionNestingLimitValidator.newInstance(0))
169+
.build();
170+
171+
CelValidationException e =
172+
assertThrows(CelValidationException.class, () -> celValidator.validate(ast).getAst());
173+
174+
assertThat(e.getMessage()).contains("comprehension nesting exceeds the configured limit: 0.");
175+
}
176+
}

validator/validators/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ java_library(
2929
name = "ast_depth_limit_validator",
3030
exports = ["//validator/src/main/java/dev/cel/validator/validators:ast_depth_limit_validator"],
3131
)
32+
33+
java_library(
34+
name = "comprehension_nesting_limit_validator",
35+
exports = ["//validator/src/main/java/dev/cel/validator/validators:comprehension_nesting_limit_validator"],
36+
)

0 commit comments

Comments
 (0)