Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ mvn-artifacts

*.swp
*.lock
.eclipse
.vscode
31 changes: 24 additions & 7 deletions checker/src/main/java/dev/cel/checker/Env.java
Original file line number Diff line number Diff line change
Expand Up @@ -451,17 +451,28 @@ public Env add(String name, Type type) {
* until the root package is reached. If {@code container} starts with {@code .}, the resolution
* is in the root container only.
*
* <p>Returns {@code null} if the function cannot be found.
* <p>Returns {@code null} if the ident cannot be found.
*/
public @Nullable CelIdentDecl tryLookupCelIdent(CelContainer container, String name) {
// Attempt to find the decl with just the ident name to account for shadowed variables
CelIdentDecl decl = tryLookupCelIdentFromLocalScopes(name);
if (decl != null) {
return decl;
// A name with a leading '.' always resolves in the root scope, bypassing local scopes.
if (!name.startsWith(".")) {
// Check if this is a qualified ident, or a field selection.
String simpleName = name;
int dotIndex = name.indexOf('.');
if (dotIndex > 0) {
simpleName = name.substring(0, dotIndex);
}

// Attempt to find the decl with just the ident name to account for shadowed variables.
CelIdentDecl decl = tryLookupCelIdentFromLocalScopes(simpleName);
if (decl != null) {
// Null signals field selection
return dotIndex > 0 ? null : decl;
}
}

for (String cand : container.resolveCandidateNames(name)) {
decl = tryLookupCelIdent(cand);
CelIdentDecl decl = tryLookupCelIdent(cand);
if (decl != null) {
return decl;
}
Expand Down Expand Up @@ -503,7 +514,13 @@ public Env add(String name, Type type) {
return null;
}

private @Nullable CelIdentDecl tryLookupCelIdentFromLocalScopes(String name) {
/**
* Lookup a local identifier by name. This searches only comprehension scopes, bypassing standard environment or user-defined environment.
*
* <p>Returns {@code null} if not found in local scopes.
*/
@Nullable
CelIdentDecl tryLookupCelIdentFromLocalScopes(String name) {
int firstUserSpaceScope = 2;
// Iterate from the top of the stack down to the first local scope.
// Note that:
Expand Down
33 changes: 28 additions & 5 deletions checker/src/main/java/dev/cel/checker/ExprChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,24 @@ private CelExpr visit(CelExpr expr, CelExpr.CelIdent ident) {
env.setRef(expr, makeReference(decl));
return expr;
}
if (!decl.name().equals(ident.name())) {

// Preserve leading dot to signal runtime to bypass local scopes.
String refName = decl.name();
if (ident.name().startsWith(".")) {
refName = "." + refName;
}

if (!refName.equals(ident.name())) {
// Overwrite the identifier with its fully qualified name.
expr = replaceIdentSubtree(expr, decl.name());
expr = replaceIdentSubtree(expr, refName);
}
env.setType(expr, decl.type());
env.setRef(expr, makeReference(decl));
// Build reference with the (potentially prefixed) name, preserving constant value if present.
CelReference.Builder refBuilder = CelReference.newBuilder().setName(refName);
if (decl.constant().isPresent()) {
refBuilder.setValue(decl.constant().get());
}
env.setRef(expr, refBuilder.build());
return expr;
}

Expand All @@ -260,13 +272,24 @@ private CelExpr visit(CelExpr expr, CelExpr.CelSelect select) {
env.reportError(expr.id(), getPosition(expr), "expression does not select a field");
env.setType(expr, SimpleType.BOOL);
} else {
// Preserve leading dot to signal runtime to bypass local scopes.
String refName = decl.name();
if (qname.startsWith(".")) {
refName = "." + refName;
}

if (namespacedDeclarations) {
// Rewrite the node to be a variable reference to the resolved fully-qualified
// variable name.
expr = replaceIdentSubtree(expr, decl.name());
expr = replaceIdentSubtree(expr, refName);
}
env.setType(expr, decl.type());
env.setRef(expr, makeReference(decl));
// Build reference with the (potentially prefixed) name, preserving constant value.
CelReference.Builder refBuilder = CelReference.newBuilder().setName(refName);
if (decl.constant().isPresent()) {
refBuilder.setValue(decl.constant().get());
}
env.setRef(expr, refBuilder.build());
}
return expr;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ Optional<AccumulatedUnknowns> maybePartialUnknown(CelAttribute attribute) {

/** Resolve a simple name to a value. */
DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId) {
// Strip leading dot if present (for global disambiguation).
if (name.startsWith(".")) {
name = name.substring(1);
}

CelAttribute attr = CelAttribute.EMPTY;

if (attributeTrackingEnabled) {
Expand Down Expand Up @@ -154,6 +159,10 @@ private ScopedResolver(

@Override
DefaultInterpreter.IntermediateResult resolveSimpleName(String name, Long exprId) {
// A name with a leading '.' always resolves in the root scope
if (name.startsWith(".")) {
return parent.resolveSimpleName(name, exprId);
}
DefaultInterpreter.IntermediateResult result = lazyEvalResultCache.get(name);
if (result != null) {
return copyIfMutable(result);
Expand Down
34 changes: 33 additions & 1 deletion runtime/src/test/resources/comprehension.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,36 @@ declare com.x {
}
=====>
bindings: {com.x=1}
result: true
result: true

Source: [{'z': 0}].exists(y, y.z == 0)
declare cel.example.y {
value int
}
=====>
bindings: {cel.example.y={z=1}}
result: true

Source: [{'z': 0}].exists(y, y.z == 0 && .y.z == 1)
declare y.z {
value int
}
=====>
bindings: {y.z=1}
result: true

Source: [0].exists(x, x == 0 && .x == 1)
declare x {
value int
}
=====>
bindings: {x=1}
result: true

Source: [0].exists(x, [x+1].exists(x, x == .x))
declare x {
value int
}
=====>
bindings: {x=1}
result: true
20 changes: 20 additions & 0 deletions testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,26 @@ public void comprehension() throws Exception {
container = CelContainer.ofName("com");
source = "[0].exists(x, x == 0)";
runTest(ImmutableMap.of("com.x", 1));

clearAllDeclarations();
declareVariable("cel.example.y", SimpleType.INT);
container = CelContainer.ofName("cel.example");
source = "[{'z': 0}].exists(y, y.z == 0)";
runTest(ImmutableMap.of("cel.example.y", ImmutableMap.of("z", 1)));

clearAllDeclarations();
declareVariable("y.z", SimpleType.INT);
container = CelContainer.ofName("y");
source = "[{'z': 0}].exists(y, y.z == 0 && .y.z == 1)";
runTest(ImmutableMap.of("y.z", 1));

clearAllDeclarations();
declareVariable("x", SimpleType.INT);
source = "[0].exists(x, x == 0 && .x == 1)";
runTest(ImmutableMap.of("x", 1));

source = "[0].exists(x, [x+1].exists(x, x == .x))";
runTest(ImmutableMap.of("x", 1));
}

@Test
Expand Down
3 changes: 2 additions & 1 deletion testing/src/main/java/dev/cel/testing/BaselineTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ protected void verify() {
String expected = getExpected().trim();
LineDiffer.Diff lineDiff = LineDiffer.diffLines(expected, actual);
if (!lineDiff.isEmpty()) {
String actualFileLocation = tryCreateNewBaseline(actual);
// String actualFileLocation = tryCreateNewBaseline(actual);
String actualFileLocation = "foo";
throw new BaselineComparisonError(
testName.getMethodName(), baselineFileName(), actual, actualFileLocation, lineDiff);
}
Expand Down
Loading