Skip to content

Commit ce9f8f3

Browse files
l46kokcopybara-github
authored andcommitted
Introduce common helpers for handling AST navigation
PiperOrigin-RevId: 960507413
1 parent 2da98c9 commit ce9f8f3

9 files changed

Lines changed: 563 additions & 51 deletions

File tree

common/navigation/BUILD.bazel

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,13 @@ java_library(
2525
name = "mutable_navigation",
2626
exports = ["//common/src/main/java/dev/cel/common/navigation:mutable_navigation"],
2727
)
28+
29+
java_library(
30+
name = "expr_util",
31+
exports = ["//common/src/main/java/dev/cel/common/navigation:expr_util"],
32+
)
33+
34+
cel_android_library(
35+
name = "expr_util_android",
36+
exports = ["//common/src/main/java/dev/cel/common/navigation:expr_util_android"],
37+
)

common/src/main/java/dev/cel/common/navigation/BUILD.bazel

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,36 @@ cel_android_library(
4848
],
4949
)
5050

51+
java_library(
52+
name = "expr_util",
53+
srcs = [
54+
"CelNavigableExprUtil.java",
55+
],
56+
tags = [
57+
],
58+
deps = [
59+
":common",
60+
"//common/ast",
61+
"@maven//:com_google_errorprone_error_prone_annotations",
62+
"@maven//:com_google_guava_guava",
63+
],
64+
)
65+
66+
cel_android_library(
67+
name = "expr_util_android",
68+
srcs = [
69+
"CelNavigableExprUtil.java",
70+
],
71+
tags = [
72+
],
73+
deps = [
74+
":common_android",
75+
"//common/ast:ast_android",
76+
"@maven//:com_google_errorprone_error_prone_annotations",
77+
"@maven_android//:com_google_guava_guava",
78+
],
79+
)
80+
5181
java_library(
5282
name = "navigation",
5383
srcs = [

common/src/main/java/dev/cel/common/navigation/BaseNavigableExpr.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import com.google.errorprone.annotations.CanIgnoreReturnValue;
1818
import com.google.errorprone.annotations.CheckReturnValue;
19+
import com.google.errorprone.annotations.DoNotMock;
1920
import dev.cel.common.ast.CelExpr;
2021
import dev.cel.common.ast.CelExpr.ExprKind;
2122
import dev.cel.common.ast.Expression;
@@ -25,9 +26,15 @@
2526
/**
2627
* BaseNavigableExpr represents the base navigable expression value with methods to inspect the
2728
* parent and child expressions.
29+
*
30+
* <p>This class is intentionally non-extensible outside of the {@code dev.cel.common.navigation}
31+
* package.
2832
*/
33+
@DoNotMock("Use CelNavigableExpr or CelNavigableMutableExpr")
2934
@SuppressWarnings("unchecked") // Generic types are properly bound to Expression
30-
abstract class BaseNavigableExpr<E extends Expression> {
35+
public abstract class BaseNavigableExpr<E extends Expression> {
36+
37+
BaseNavigableExpr() {}
3138

3239
public abstract E expr();
3340

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
package dev.cel.common.navigation;
16+
17+
import static com.google.common.base.Preconditions.checkNotNull;
18+
19+
import com.google.errorprone.annotations.CheckReturnValue;
20+
import dev.cel.common.ast.CelExpr.ExprKind.Kind;
21+
import dev.cel.common.ast.Expression;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.Optional;
25+
26+
/** Utility class for common AST navigation and scoping inspections on {@link BaseNavigableExpr}. */
27+
@CheckReturnValue
28+
public final class CelNavigableExprUtil {
29+
30+
/**
31+
* Returns true if {@code variableName} is in scope and shadowed by an enclosing comprehension
32+
* above {@code expr}.
33+
*
34+
* <p>A variable is shadowed at {@code expr} if an ancestor comprehension declares it as an
35+
* iteration variable ({@code iterVar}, {@code iterVar2}) or accumulator variable ({@code
36+
* accuVar}) and {@code expr} resides within a branch where that variable is active:
37+
*
38+
* <ul>
39+
* <li>In {@code loopCondition} and {@code loopStep}: {@code iterVar}, {@code iterVar2}, and
40+
* {@code accuVar} are in scope.
41+
* <li>In {@code result}: only {@code accuVar} is in scope ({@code iterVar} and {@code iterVar2}
42+
* have fallen out of scope).
43+
* <li>In {@code iterRange} and {@code accuInit}: none of the comprehension variables are in
44+
* scope.
45+
* </ul>
46+
*
47+
* <p>For example, in the expression:
48+
*
49+
* <pre>{@code
50+
* [1, 2].all(x, x > 0)
51+
* }</pre>
52+
*
53+
* <ul>
54+
* <li>At {@code x} in {@code x > 0}: {@code isVariableShadowed(x, "x")} is {@code true}.
55+
* <li>At the list {@code [1, 2]}: {@code isVariableShadowed(list, "x")} is {@code false}.
56+
* </ul>
57+
*/
58+
public static boolean isVariableShadowed(BaseNavigableExpr<?> expr, String variableName) {
59+
return areVariablesShadowed(expr, Collections.singleton(variableName));
60+
}
61+
62+
/**
63+
* Returns true if any of {@code variableNames} is in scope and shadowed by an enclosing
64+
* comprehension above {@code expr}.
65+
*
66+
* <p>For example, in the nested comprehension expression:
67+
*
68+
* <pre>{@code
69+
* [1, 2].all(x, [3, 4].all(y, x > 0 && y > 0))
70+
* }</pre>
71+
*
72+
* At {@code y > 0}, {@code areVariablesShadowed(node, ImmutableSet.of("x", "z"))} is {@code true}
73+
* because {@code x} is in scope from the outer comprehension.
74+
*/
75+
@SuppressWarnings("ReferenceEquality") // Required to disambiguate child branches
76+
public static boolean areVariablesShadowed(
77+
BaseNavigableExpr<?> expr, Collection<String> variableNames) {
78+
checkNotNull(expr);
79+
checkNotNull(variableNames);
80+
if (variableNames.isEmpty()) {
81+
return false;
82+
}
83+
BaseNavigableExpr<?> curr = expr;
84+
Optional<? extends BaseNavigableExpr<?>> maybeParent = curr.parent();
85+
while (maybeParent.isPresent()) {
86+
BaseNavigableExpr<?> parent = maybeParent.get();
87+
if (parent.getKind() == Kind.COMPREHENSION) {
88+
Expression.Comprehension<?> comp = parent.expr().comprehension();
89+
Expression currExpr = curr.expr();
90+
91+
if (currExpr != comp.iterRange() && currExpr != comp.accuInit()) {
92+
if (currExpr == comp.result()) {
93+
if (variableNames.contains(comp.accuVar())) {
94+
return true;
95+
}
96+
} else {
97+
if (variableNames.contains(comp.iterVar())
98+
|| variableNames.contains(comp.iterVar2())
99+
|| variableNames.contains(comp.accuVar())) {
100+
return true;
101+
}
102+
}
103+
}
104+
}
105+
curr = parent;
106+
maybeParent = parent.parent();
107+
}
108+
return false;
109+
}
110+
111+
/**
112+
* Returns true if {@code expr} is an {@code IDENT} node that references a variable declared by an
113+
* enclosing comprehension.
114+
*
115+
* <p>For example, in the expression:
116+
*
117+
* <pre>{@code
118+
* [a].all(x, x > a)
119+
* }</pre>
120+
*
121+
* <ul>
122+
* <li>At identifier {@code x}: {@code isComprehensionVariable(x)} is {@code true}.
123+
* <li>At identifier {@code a}: {@code isComprehensionVariable(a)} is {@code false}.
124+
* </ul>
125+
*/
126+
public static boolean isComprehensionVariable(BaseNavigableExpr<?> expr) {
127+
checkNotNull(expr);
128+
return expr.getKind() == Kind.IDENT
129+
&& areVariablesShadowed(expr, Collections.singleton(expr.expr().ident().name()));
130+
}
131+
132+
/**
133+
* Returns true if {@code expr} or any identifier within {@code expr} references a variable
134+
* declared by an enclosing comprehension.
135+
*
136+
* <p>For example, in the expression:
137+
*
138+
* <pre>{@code
139+
* [a].all(x, x > a)
140+
* }</pre>
141+
*
142+
* <ul>
143+
* <li>At the subtree {@code x > a}: {@code hasComprehensionVariable(subtree)} is {@code true}
144+
* because {@code x} is a comprehension variable.
145+
* <li>At the subtree {@code [a]}: {@code hasComprehensionVariable(iterRange)} is {@code false}.
146+
* </ul>
147+
*/
148+
public static boolean hasComprehensionVariable(BaseNavigableExpr<?> expr) {
149+
checkNotNull(expr);
150+
return expr.allNodes()
151+
.filter(node -> node.getKind() == Kind.IDENT)
152+
.anyMatch(CelNavigableExprUtil::isComprehensionVariable);
153+
}
154+
155+
private CelNavigableExprUtil() {}
156+
}

common/src/test/java/dev/cel/common/navigation/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ java_library(
2121
"//common/ast:mutable_expr",
2222
"//common/navigation",
2323
"//common/navigation:common",
24+
"//common/navigation:expr_util",
2425
"//common/navigation:mutable_navigation",
2526
"//common/types",
2627
"//compiler",
2728
"//compiler:compiler_builder",
29+
"//extensions",
2830
"//parser:macro",
2931
"@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto",
3032
"@maven//:com_google_guava_guava",

0 commit comments

Comments
 (0)