Skip to content

Commit 9fd4d79

Browse files
jnthntatumcopybara-github
authored andcommitted
Add comprehension nesting limit validator.
PiperOrigin-RevId: 899255266
1 parent 072542b commit 9fd4d79

4 files changed

Lines changed: 228 additions & 0 deletions

File tree

validator/BUILD

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,33 @@ cc_test(
182182
],
183183
)
184184

185+
cc_library(
186+
name = "comprehension_nesting_validator",
187+
srcs = ["comprehension_nesting_validator.cc"],
188+
hdrs = ["comprehension_nesting_validator.h"],
189+
deps = [
190+
":validator",
191+
"//common:expr",
192+
"//common:navigable_ast",
193+
"@com_google_absl//absl/log:absl_check",
194+
"@com_google_absl//absl/strings",
195+
],
196+
)
197+
198+
cc_test(
199+
name = "comprehension_nesting_validator_test",
200+
srcs = ["comprehension_nesting_validator_test.cc"],
201+
deps = [
202+
":comprehension_nesting_validator",
203+
":validator",
204+
"//compiler",
205+
"//compiler:compiler_factory",
206+
"//compiler:standard_library",
207+
"//extensions:bindings_ext",
208+
"//internal:testing",
209+
"//internal:testing_descriptor_pool",
210+
"@com_google_absl//absl/status:statusor",
211+
],
212+
)
213+
185214
licenses(["notice"])
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2026 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+
#include "validator/comprehension_nesting_validator.h"
16+
17+
#include "absl/log/absl_check.h"
18+
#include "absl/strings/str_cat.h"
19+
#include "common/expr.h"
20+
#include "common/navigable_ast.h"
21+
#include "validator/validator.h"
22+
23+
namespace cel {
24+
25+
namespace {
26+
27+
bool IsEmptyRangeComprehension(const NavigableAstNode& node) {
28+
ABSL_DCHECK(node.expr()->has_comprehension_expr());
29+
const auto& comp = node.expr()->comprehension_expr();
30+
return comp.has_iter_range() && comp.iter_range().has_list_expr() &&
31+
comp.iter_range().list_expr().elements().empty();
32+
}
33+
34+
} // namespace
35+
36+
Validation ComprehensionNestingLimitValidator(int limit) {
37+
return Validation(
38+
[limit](ValidationContext& context) -> bool {
39+
bool is_valid = true;
40+
for (const auto& node :
41+
context.navigable_ast().Root().DescendantsPostorder()) {
42+
if (node.node_kind() != NodeKind::kComprehension) {
43+
continue;
44+
}
45+
if (IsEmptyRangeComprehension(node)) {
46+
continue;
47+
}
48+
49+
int count = 0;
50+
const NavigableAstNode* current = &node;
51+
while (current != nullptr) {
52+
if (current->node_kind() == NodeKind::kComprehension &&
53+
!IsEmptyRangeComprehension(*current)) {
54+
count++;
55+
}
56+
current = current->parent();
57+
}
58+
if (count > limit) {
59+
context.ReportErrorAt(
60+
node.expr()->id(),
61+
absl::StrCat("comprehension nesting level of ", count,
62+
" exceeds limit of ", limit));
63+
is_valid = false;
64+
break;
65+
}
66+
}
67+
return is_valid;
68+
},
69+
"cel.validator.comprehension_nesting_limit");
70+
}
71+
72+
} // namespace cel
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2026 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+
#ifndef THIRD_PARTY_CEL_CPP_VALIDATOR_COMPREHENSION_NESTING_VALIDATOR_H_
16+
#define THIRD_PARTY_CEL_CPP_VALIDATOR_COMPREHENSION_NESTING_VALIDATOR_H_
17+
18+
#include "validator/validator.h"
19+
20+
namespace cel {
21+
22+
// Returns a `Validation` that checks that comprehensions are not nested beyond
23+
// the specified limit.
24+
//
25+
// Comprehensions with an empty iteration range (e.g. `cel.bind`) do not count
26+
// towards the nesting limit.
27+
Validation ComprehensionNestingLimitValidator(int limit);
28+
29+
} // namespace cel
30+
31+
#endif // THIRD_PARTY_CEL_CPP_VALIDATOR_COMPREHENSION_NESTING_VALIDATOR_H_
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2026 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+
#include "validator/comprehension_nesting_validator.h"
16+
17+
#include <memory>
18+
#include <string>
19+
#include <utility>
20+
21+
#include "absl/status/statusor.h"
22+
#include "compiler/compiler.h"
23+
#include "compiler/compiler_factory.h"
24+
#include "compiler/standard_library.h"
25+
#include "extensions/bindings_ext.h"
26+
#include "internal/testing.h"
27+
#include "internal/testing_descriptor_pool.h"
28+
#include "validator/validator.h"
29+
30+
namespace cel {
31+
namespace {
32+
33+
using ::testing::HasSubstr;
34+
35+
absl::StatusOr<std::unique_ptr<Compiler>> StdLibCompiler() {
36+
CEL_ASSIGN_OR_RETURN(
37+
auto builder,
38+
NewCompilerBuilder(internal::GetSharedTestingDescriptorPool()));
39+
CEL_RETURN_IF_ERROR(builder->AddLibrary(StandardCompilerLibrary()));
40+
CEL_RETURN_IF_ERROR(
41+
builder->AddLibrary(cel::extensions::BindingsCompilerLibrary()));
42+
return builder->Build();
43+
}
44+
45+
struct TestCase {
46+
std::string expression;
47+
int limit;
48+
bool valid;
49+
std::string error_substr = "";
50+
};
51+
52+
using ComprehensionNestingValidatorTest = testing::TestWithParam<TestCase>;
53+
54+
TEST_P(ComprehensionNestingValidatorTest, Validate) {
55+
const auto& test_case = GetParam();
56+
Validator validator;
57+
validator.AddValidation(ComprehensionNestingLimitValidator(test_case.limit));
58+
59+
ASSERT_OK_AND_ASSIGN(auto compiler, StdLibCompiler());
60+
auto result_or = compiler->Compile(test_case.expression);
61+
if (!result_or.ok()) {
62+
GTEST_SKIP() << "Expression failed to compile: " << test_case.expression
63+
<< " " << result_or.status().message();
64+
}
65+
auto result = std::move(result_or).value();
66+
67+
validator.UpdateValidationResult(result);
68+
69+
EXPECT_EQ(result.IsValid(), test_case.valid)
70+
<< "Expression: " << test_case.expression
71+
<< " Limit: " << test_case.limit;
72+
if (!test_case.valid) {
73+
EXPECT_THAT(result.FormatError(), HasSubstr(test_case.error_substr));
74+
}
75+
}
76+
77+
INSTANTIATE_TEST_SUITE_P(
78+
ComprehensionNestingValidatorTest, ComprehensionNestingValidatorTest,
79+
testing::Values(
80+
TestCase{"[1, 2].all(x, x > 0)", 1, true},
81+
TestCase{"[1, 2].all(x, [1, 2].all(y, x > y))", 1, false,
82+
"comprehension nesting level of 2 exceeds limit of 1"},
83+
TestCase{"[1, 2].all(x, [1, 2].all(y, x > y))", 2, true},
84+
// Empty range comprehension (does not count)
85+
TestCase{"[].all(x, [1, 2].all(y, y > 0))", 1, true},
86+
TestCase{"cel.bind(x, [1, 2].all(y, y > 0), [1, 2].all(z, z > 0))", 1,
87+
true},
88+
// Nested empty range comprehensions
89+
TestCase{"[].all(x, [].all(y, true))", 0, true},
90+
// Deeply nested mixed
91+
TestCase{"[1].all(x, [].all(y, [2].all(z, true)))", 1, false,
92+
"comprehension nesting level of 2 exceeds limit of 1"},
93+
TestCase{"[1].all(x, [].all(y, [2].all(z, true)))", 2, true}));
94+
95+
} // namespace
96+
} // namespace cel

0 commit comments

Comments
 (0)