Skip to content

Commit e7c1ad3

Browse files
Add type checking for ident expressions (#279)
Co-authored-by: Sri Krishna <skrishna@buf.build>
1 parent 0783a6e commit e7c1ad3

3 files changed

Lines changed: 150 additions & 13 deletions

File tree

packages/cel/src/check.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const cache = new WeakMap<CelEnv, Checker>();
3232
export function check(env: CelEnv, expr: Expr | ParsedExpr): CheckedExpr {
3333
let checker = cache.get(env);
3434
if (checker === undefined) {
35-
checker = new Checker();
35+
checker = new Checker(env);
3636
cache.set(env, checker);
3737
}
3838
if (isMessage(expr, ExprSchema)) {

packages/cel/src/checker.test.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,9 @@ import {
2020
} from "./testing.js";
2121

2222
const filter = createExpressionFilter([
23-
// Ident types
24-
"is",
25-
"ii",
26-
"iu",
27-
"iz",
28-
"ib",
29-
"id",
30-
"ix",
3123
"[]",
3224
"[1]",
3325
'[1, "A"]',
34-
35-
// Call resolution
3626
"fg_s()",
3727
"is.fi_s_s()",
3828
"1 + 2",
@@ -74,7 +64,6 @@ const filter = createExpressionFilter([
7464
`lists.filter(x, x > 1.5)`,
7565
`.google.expr.proto3.test.TestAllTypes`,
7666
`test.TestAllTypes`,
77-
`x`,
7867
`list == type([1]) && map == type({1:2u})`,
7968
`myfun(1, true, 3u) + 1.myfun(false, 3u).myfun(true, 42u)`,
8069
`size(x) > 4`,

packages/cel/src/checker.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,23 @@ import type {
1717
Constant,
1818
Expr,
1919
SourceInfo,
20+
Expr_Ident,
21+
ConstantSchema,
2022
} from "@bufbuild/cel-spec/cel/expr/syntax_pb.js";
2123
import {
2224
type CheckedExpr,
2325
CheckedExprSchema,
26+
type ReferenceSchema,
2427
type Type,
2528
type TypeSchema,
2629
Type_PrimitiveType,
2730
} from "@bufbuild/cel-spec/cel/expr/checked_pb.js";
2831
import { create, type MessageInitShape } from "@bufbuild/protobuf";
2932
import {
3033
CelScalar,
34+
celType,
3135
type CelType,
36+
type CelValue,
3237
DURATION,
3338
listType,
3439
type mapKeyType,
@@ -37,17 +42,33 @@ import {
3742
TIMESTAMP,
3843
} from "./type.js";
3944
import { NullValue } from "@bufbuild/protobuf/wkt";
45+
import type { CelEnv } from "./env.js";
46+
import { resolveCandidateNames } from "./namespace.js";
47+
import { celError } from "./error.js";
48+
import { isCelUint } from "./uint.js";
49+
import { createScope } from "./scope.js";
50+
51+
const noopScope = createScope();
4052

4153
export class Checker {
54+
private readonly referenceMap: Map<
55+
bigint,
56+
MessageInitShape<typeof ReferenceSchema>
57+
> = new Map();
4258
private readonly typeMap: Map<bigint, CelType> = new Map();
59+
private scope = noopScope;
60+
61+
constructor(private readonly env: CelEnv) {}
4362

4463
check(expr: Expr, sourceInfo: SourceInfo | undefined): CheckedExpr {
4564
// Clear each time we check since Checker instances are cached per environment.
4665
this.typeMap.clear();
66+
this.scope = noopScope;
67+
this.referenceMap.clear();
4768
return create(CheckedExprSchema, {
4869
expr: this.checkExpr(expr),
4970
sourceInfo,
50-
// TODO: referenceMap
71+
referenceMap: celReferenceMapToProtoReferenceMap(this.referenceMap),
5172
typeMap: celTypeMapToProtoTypeMap(this.typeMap),
5273
});
5374
}
@@ -56,6 +77,8 @@ export class Checker {
5677
switch (expr.exprKind.case) {
5778
case "constExpr":
5879
return this.checkConstExpr(expr.id, expr.exprKind.value);
80+
case "identExpr":
81+
return this.checkIdentExpr(expr.id, expr.exprKind.value);
5982
default:
6083
throw new Error(`Unsupported expression kind: ${expr.exprKind.case}`);
6184
}
@@ -107,9 +130,75 @@ export class Checker {
107130
};
108131
}
109132

133+
private checkIdentExpr(
134+
id: bigint,
135+
ident: Expr_Ident,
136+
): MessageInitShape<typeof ExprSchema> {
137+
const variable = this.resolveVariable(ident.name);
138+
if (variable === undefined) {
139+
throw celError(
140+
`undeclared reference to '${ident.name}' (in container '${this.env.namespace}')`,
141+
id,
142+
);
143+
}
144+
this.setType(id, variable.type);
145+
this.setReference(id, identReference(variable.name));
146+
return {
147+
id,
148+
exprKind: {
149+
case: "identExpr",
150+
value: {
151+
name: variable.name,
152+
},
153+
},
154+
};
155+
}
156+
110157
private setType(id: bigint, type: CelType): void {
111158
this.typeMap.set(id, type);
112159
}
160+
161+
private setReference(
162+
id: bigint,
163+
reference: MessageInitShape<typeof ReferenceSchema>,
164+
): void {
165+
this.referenceMap.set(id, reference);
166+
}
167+
168+
/**
169+
* Resolves a variable according to the CEL name resolution rules.
170+
*
171+
* See https://github.com/google/cel-spec/blob/master/doc/langdef.md#name-resolution
172+
*/
173+
private resolveVariable(name: string):
174+
| {
175+
name: string;
176+
type: CelType;
177+
}
178+
| undefined {
179+
// First we check for the variable to be in
180+
// the comprehension scope chain if it is not global.
181+
if (!name.startsWith(".")) {
182+
const type = this.scope.find(name);
183+
if (type !== undefined) {
184+
return {
185+
type,
186+
name,
187+
};
188+
}
189+
}
190+
// It can be a global because either it is fully qualified or missing in comprehension scope.
191+
for (const candidate of resolveCandidateNames(this.env.namespace, name)) {
192+
const type = this.env.variables.find(candidate);
193+
if (type) {
194+
return {
195+
type,
196+
name: candidate, // This is an optimization that allows us to partially skip name resolution during eval.
197+
};
198+
}
199+
}
200+
return undefined;
201+
}
113202
}
114203

115204
export function protoTypeToCelType(pt: Type): CelType {
@@ -272,3 +361,62 @@ function celTypeMapToProtoTypeMap(
272361
}
273362
return protoTypeMap;
274363
}
364+
365+
function identReference(
366+
name: string,
367+
value?: CelValue,
368+
): MessageInitShape<typeof ReferenceSchema> {
369+
return {
370+
name,
371+
value: value ? celValueToProtoConstant(value) : undefined,
372+
};
373+
}
374+
375+
function protoConstant<
376+
T extends Exclude<Constant["constantKind"]["case"], undefined>,
377+
>(
378+
caseName: T,
379+
value: Extract<Constant["constantKind"], { case: T }>["value"],
380+
): MessageInitShape<typeof ConstantSchema> {
381+
return {
382+
constantKind: { case: caseName, value } as Constant["constantKind"],
383+
};
384+
}
385+
386+
function celValueToProtoConstant(
387+
value: CelValue,
388+
): MessageInitShape<typeof ConstantSchema> {
389+
switch (typeof value) {
390+
case "bigint":
391+
return protoConstant("int64Value", value);
392+
case "number":
393+
return protoConstant("doubleValue", value);
394+
case "boolean":
395+
return protoConstant("boolValue", value);
396+
case "string":
397+
return protoConstant("stringValue", value);
398+
case "object":
399+
switch (true) {
400+
case isCelUint(value):
401+
return protoConstant("uint64Value", value.value);
402+
case null:
403+
return protoConstant("nullValue", NullValue.NULL_VALUE);
404+
case value instanceof Uint8Array:
405+
return protoConstant("bytesValue", value);
406+
}
407+
}
408+
throw new Error(`unsupported constant type: ${celType(value)}`);
409+
}
410+
411+
function celReferenceMapToProtoReferenceMap(
412+
referenceMap: Map<bigint, MessageInitShape<typeof ReferenceSchema>>,
413+
): Record<string, MessageInitShape<typeof ReferenceSchema>> {
414+
const protoReferenceMap: Record<
415+
string,
416+
MessageInitShape<typeof ReferenceSchema>
417+
> = {};
418+
for (const [id, ref] of referenceMap.entries()) {
419+
protoReferenceMap[id.toString()] = ref;
420+
}
421+
return protoReferenceMap;
422+
}

0 commit comments

Comments
 (0)