Skip to content

Commit 0386196

Browse files
jnthntatumcopybara-github
authored andcommitted
Add support for quoted field selectors in java.
PiperOrigin-RevId: 712716800
1 parent df1bee1 commit 0386196

File tree

10 files changed

+226
-32
lines changed

10 files changed

+226
-32
lines changed

common/src/main/java/dev/cel/common/CelOptions.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public enum ProtoUnsetFieldOptions {
6969

7070
public abstract boolean enableHiddenAccumulatorVar();
7171

72+
public abstract boolean enableQuotedIdentifierSyntax();
73+
7274
// Type-Checker related options
7375

7476
public abstract boolean enableCompileTimeOverloadResolution();
@@ -191,6 +193,7 @@ public static Builder newBuilder() {
191193
.retainRepeatedUnaryOperators(false)
192194
.retainUnbalancedLogicalExpressions(false)
193195
.enableHiddenAccumulatorVar(false)
196+
.enableQuotedIdentifierSyntax(false)
194197
// Type-Checker options
195198
.enableCompileTimeOverloadResolution(false)
196199
.enableHomogeneousLiterals(false)
@@ -332,6 +335,16 @@ public abstract static class Builder {
332335
*/
333336
public abstract Builder enableHiddenAccumulatorVar(boolean value);
334337

338+
/**
339+
* Enable quoted identifier syntax.
340+
*
341+
* <p>This enables the use of quoted identifier syntax when parsing CEL expressions. When
342+
* enabled, the parser will accept identifiers that are surrounded by backticks (`) and will
343+
* treat them as a single identifier. Currently, this is only supported for field specifiers
344+
* over a limited character set.
345+
*/
346+
public abstract Builder enableQuotedIdentifierSyntax(boolean value);
347+
335348
// Type-Checker related options
336349

337350
/**

parser/src/main/java/dev/cel/parser/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ java_library(
127127
"//common",
128128
"//common/ast",
129129
"//common/ast:cel_expr_visitor",
130+
"@maven//:com_google_guava_guava",
130131
"@maven//:com_google_protobuf_protobuf_java",
132+
"@maven//:com_google_re2j_re2j",
131133
],
132134
)

parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
// limitations under the License.
1414
package dev.cel.parser;
1515

16+
import com.google.common.collect.ImmutableSet;
1617
import com.google.protobuf.ByteString;
18+
import com.google.re2j.Pattern;
1719
import dev.cel.common.CelAbstractSyntaxTree;
1820
import dev.cel.common.CelSource;
1921
import dev.cel.common.ast.CelConstant;
@@ -43,6 +45,10 @@ public class CelUnparserVisitor extends CelExprVisitor {
4345
protected static final String RIGHT_BRACE = "}";
4446
protected static final String COLON = ":";
4547
protected static final String QUESTION_MARK = "?";
48+
protected static final String BACKTICK = "`";
49+
private static final Pattern IDENTIFIER_SEGMENT_PATTERN =
50+
Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
51+
private static final ImmutableSet<String> RESTRICTED_FIELD_NAMES = ImmutableSet.of("in");
4652

4753
protected final CelAbstractSyntaxTree ast;
4854
protected final CelSource sourceInfo;
@@ -60,6 +66,14 @@ public String unparse() {
6066
return stringBuilder.toString();
6167
}
6268

69+
private static String maybeQuoteField(String field) {
70+
if (RESTRICTED_FIELD_NAMES.contains(field)
71+
|| !IDENTIFIER_SEGMENT_PATTERN.matcher(field).matches()) {
72+
return BACKTICK + field + BACKTICK;
73+
}
74+
return field;
75+
}
76+
6377
@Override
6478
public void visit(CelExpr expr) {
6579
if (sourceInfo.getMacroCalls().containsKey(expr.id())) {
@@ -191,7 +205,7 @@ protected void visit(CelExpr expr, CelStruct struct) {
191205
if (e.optionalEntry()) {
192206
stringBuilder.append(QUESTION_MARK);
193207
}
194-
stringBuilder.append(e.fieldKey());
208+
stringBuilder.append(maybeQuoteField(e.fieldKey()));
195209
stringBuilder.append(COLON).append(SPACE);
196210
visit(e.value());
197211
}
@@ -263,7 +277,7 @@ private void visitSelect(CelExpr operand, boolean testOnly, String op, String fi
263277
}
264278
boolean nested = !testOnly && isBinaryOrTernaryOperator(operand);
265279
visitMaybeNested(operand, nested);
266-
stringBuilder.append(op).append(field);
280+
stringBuilder.append(op).append(maybeQuoteField(field));
267281
if (testOnly) {
268282
stringBuilder.append(RIGHT_PAREN);
269283
}

parser/src/main/java/dev/cel/parser/Parser.java

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@
3333
import cel.parser.internal.CELParser.CreateMapContext;
3434
import cel.parser.internal.CELParser.CreateMessageContext;
3535
import cel.parser.internal.CELParser.DoubleContext;
36+
import cel.parser.internal.CELParser.EscapeIdentContext;
37+
import cel.parser.internal.CELParser.EscapedIdentifierContext;
3638
import cel.parser.internal.CELParser.ExprContext;
3739
import cel.parser.internal.CELParser.ExprListContext;
3840
import cel.parser.internal.CELParser.FieldInitializerListContext;
39-
import cel.parser.internal.CELParser.IdentOrGlobalCallContext;
41+
import cel.parser.internal.CELParser.GlobalCallContext;
42+
import cel.parser.internal.CELParser.IdentContext;
4043
import cel.parser.internal.CELParser.IndexContext;
4144
import cel.parser.internal.CELParser.IntContext;
4245
import cel.parser.internal.CELParser.ListInitContext;
@@ -52,6 +55,7 @@
5255
import cel.parser.internal.CELParser.PrimaryExprContext;
5356
import cel.parser.internal.CELParser.RelationContext;
5457
import cel.parser.internal.CELParser.SelectContext;
58+
import cel.parser.internal.CELParser.SimpleIdentifierContext;
5559
import cel.parser.internal.CELParser.StartContext;
5660
import cel.parser.internal.CELParser.StringContext;
5761
import cel.parser.internal.CELParser.UintContext;
@@ -438,7 +442,7 @@ public CelExpr visitSelect(SelectContext context) {
438442
if (context.id == null) {
439443
return exprFactory.newExprBuilder(context).build();
440444
}
441-
String id = context.id.getText();
445+
String id = normalizeEscapedIdent(context.id);
442446

443447
if (context.opt != null && context.opt.getText().equals("?")) {
444448
if (!options.enableOptionalSyntax()) {
@@ -535,7 +539,7 @@ public CelExpr visitCreateMessage(CreateMessageContext context) {
535539
}
536540

537541
@Override
538-
public CelExpr visitIdentOrGlobalCall(IdentOrGlobalCallContext context) {
542+
public CelExpr visitIdent(IdentContext context) {
539543
checkNotNull(context);
540544
if (context.id == null) {
541545
return exprFactory.newExprBuilder(context).build();
@@ -547,11 +551,25 @@ public CelExpr visitIdentOrGlobalCall(IdentOrGlobalCallContext context) {
547551
if (context.leadingDot != null) {
548552
id = "." + id;
549553
}
550-
if (context.op == null) {
551-
return exprFactory
552-
.newExprBuilder(context.id)
553-
.setIdent(CelExpr.CelIdent.newBuilder().setName(id).build())
554-
.build();
554+
555+
return exprFactory
556+
.newExprBuilder(context.id)
557+
.setIdent(CelExpr.CelIdent.newBuilder().setName(id).build())
558+
.build();
559+
}
560+
561+
@Override
562+
public CelExpr visitGlobalCall(GlobalCallContext context) {
563+
checkNotNull(context);
564+
if (context.id == null) {
565+
return exprFactory.newExprBuilder(context).build();
566+
}
567+
String id = context.id.getText();
568+
if (options.enableReservedIds() && RESERVED_IDS.contains(id)) {
569+
return exprFactory.reportError(context, "reserved identifier: %s", id);
570+
}
571+
if (context.leadingDot != null) {
572+
id = "." + id;
555573
}
556574

557575
return globalCallOrMacro(context, id);
@@ -671,6 +689,24 @@ private Optional<CelExpr> visitMacro(
671689
return expandedMacro;
672690
}
673691

692+
private String normalizeEscapedIdent(EscapeIdentContext context) {
693+
if (context instanceof SimpleIdentifierContext) {
694+
return ((SimpleIdentifierContext) context).getText();
695+
} else if (context instanceof EscapedIdentifierContext) {
696+
if (!options.enableQuotedIdentifierSyntax()) {
697+
exprFactory.reportError(context, "unsupported syntax '`'");
698+
return "";
699+
}
700+
String escaped = ((EscapedIdentifierContext) context).getText();
701+
return escaped.substring(1, escaped.length() - 1);
702+
}
703+
704+
// This is normally unreachable, but might happen if the parser is in an error state or if the
705+
// grammar is updated and not handled here.
706+
exprFactory.reportError(context, "unsupported identifier");
707+
return "";
708+
}
709+
674710
private CelExpr.CelStruct.Builder visitStructFields(FieldInitializerListContext context) {
675711
if (context == null
676712
|| context.cols == null
@@ -692,10 +728,10 @@ private CelExpr.CelStruct.Builder visitStructFields(FieldInitializerListContext
692728
}
693729

694730
// The field may be empty due to a prior error.
695-
if (fieldContext.IDENTIFIER() == null) {
731+
if (fieldContext.escapeIdent() == null) {
696732
return CelExpr.CelStruct.newBuilder();
697733
}
698-
String fieldName = fieldContext.IDENTIFIER().getText();
734+
String fieldName = normalizeEscapedIdent(fieldContext.escapeIdent());
699735

700736
CelExpr.CelStruct.Entry.Builder exprBuilder =
701737
CelExpr.CelStruct.Entry.newBuilder()
@@ -872,7 +908,7 @@ private CelExpr receiverCallOrMacro(MemberCallContext context, String id, CelExp
872908
return macroOrCall(context.args, context.open, id, Optional.of(member), true);
873909
}
874910

875-
private CelExpr globalCallOrMacro(IdentOrGlobalCallContext context, String id) {
911+
private CelExpr globalCallOrMacro(GlobalCallContext context, String id) {
876912
return macroOrCall(context.args, context.op, id, Optional.empty(), false);
877913
}
878914

parser/src/main/java/dev/cel/parser/gen/CEL.g4

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14+
1415
grammar CEL;
1516

1617
// Grammar Rules
@@ -44,26 +45,27 @@ calc
4445
;
4546

4647
unary
47-
: member # MemberExpr
48-
| (ops+='!')+ member # LogicalNot
49-
| (ops+='-')+ member # Negate
48+
: member # MemberExpr
49+
| (ops+='!')+ member # LogicalNot
50+
| (ops+='-')+ member # Negate
5051
;
5152

5253
member
53-
: primary # PrimaryExpr
54-
| member op='.' (opt='?')? id=IDENTIFIER # Select
55-
| member op='.' id=IDENTIFIER open='(' args=exprList? ')' # MemberCall
56-
| member op='[' (opt='?')? index=expr ']' # Index
54+
: primary # PrimaryExpr
55+
| member op='.' (opt='?')? id=escapeIdent # Select
56+
| member op='.' id=IDENTIFIER open='(' args=exprList? ')' # MemberCall
57+
| member op='[' (opt='?')? index=expr ']' # Index
5758
;
5859

5960
primary
60-
: leadingDot='.'? id=IDENTIFIER (op='(' args=exprList? ')')? # IdentOrGlobalCall
61-
| '(' e=expr ')' # Nested
62-
| op='[' elems=listInit? ','? ']' # CreateList
63-
| op='{' entries=mapInitializerList? ','? '}' # CreateMap
61+
: leadingDot='.'? id=IDENTIFIER # Ident
62+
| leadingDot='.'? id=IDENTIFIER (op='(' args=exprList? ')') # GlobalCall
63+
| '(' e=expr ')' # Nested
64+
| op='[' elems=listInit? ','? ']' # CreateList
65+
| op='{' entries=mapInitializerList? ','? '}' # CreateMap
6466
| leadingDot='.'? ids+=IDENTIFIER (ops+='.' ids+=IDENTIFIER)*
65-
op='{' entries=fieldInitializerList? ','? '}' # CreateMessage
66-
| literal # ConstantLiteral
67+
op='{' entries=fieldInitializerList? ','? '}' # CreateMessage
68+
| literal # ConstantLiteral
6769
;
6870

6971
exprList
@@ -79,13 +81,18 @@ fieldInitializerList
7981
;
8082

8183
optField
82-
: (opt='?')? IDENTIFIER
84+
: (opt='?')? escapeIdent
8385
;
8486

8587
mapInitializerList
8688
: keys+=optExpr cols+=':' values+=expr (',' keys+=optExpr cols+=':' values+=expr)*
8789
;
8890

91+
escapeIdent
92+
: id=IDENTIFIER # SimpleIdentifier
93+
| id=ESC_IDENTIFIER # EscapedIdentifier
94+
;
95+
8996
optExpr
9097
: (opt='?')? e=expr
9198
;
@@ -197,3 +204,4 @@ STRING
197204
BYTES : ('b' | 'B') STRING;
198205

199206
IDENTIFIER : (LETTER | '_') ( LETTER | DIGIT | '_')*;
207+
ESC_IDENTIFIER : '`' (LETTER | DIGIT | '_' | '.' | '-' | '/' | ' ')+ '`';

parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,18 @@ public void parser() {
205205
.setOptions(CelOptions.current().enableReservedIds(false).build())
206206
.build(),
207207
"while");
208+
CelParser parserWithQuotedFields =
209+
CelParserImpl.newBuilder()
210+
.setOptions(CelOptions.current().enableQuotedIdentifierSyntax(true).build())
211+
.build();
212+
runTest(parserWithQuotedFields, "foo.`bar`");
213+
runTest(parserWithQuotedFields, "foo.`bar-baz`");
214+
runTest(parserWithQuotedFields, "foo.`bar baz`");
215+
runTest(parserWithQuotedFields, "foo.`bar.baz`");
216+
runTest(parserWithQuotedFields, "foo.`bar/baz`");
217+
runTest(parserWithQuotedFields, "foo.`bar_baz`");
218+
runTest(parserWithQuotedFields, "foo.`in`");
219+
runTest(parserWithQuotedFields, "Struct{`in`: false}");
208220
}
209221

210222
@Test
@@ -273,6 +285,21 @@ public void parser_errors() {
273285
runTest(parserWithoutOptionalSupport, "a.?b && a[?b]");
274286
runTest(parserWithoutOptionalSupport, "Msg{?field: value} && {?'key': value}");
275287
runTest(parserWithoutOptionalSupport, "[?a, ?b]");
288+
289+
CelParser parserWithQuotedFields =
290+
CelParserImpl.newBuilder()
291+
.setOptions(CelOptions.current().enableQuotedIdentifierSyntax(true).build())
292+
.build();
293+
runTest(parserWithQuotedFields, "`bar`");
294+
runTest(parserWithQuotedFields, "foo.``");
295+
runTest(parserWithQuotedFields, "foo.`$bar`");
296+
297+
CelParser parserWithoutQuotedFields =
298+
CelParserImpl.newBuilder()
299+
.setOptions(CelOptions.current().enableQuotedIdentifierSyntax(false).build())
300+
.build();
301+
runTest(parserWithoutQuotedFields, "foo.`bar`");
302+
runTest(parserWithoutQuotedFields, "Struct{`bar`: false}");
276303
}
277304

278305
@Test

parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ public final class CelUnparserImplTest {
3939

4040
private final CelParser parser =
4141
CelParserImpl.newBuilder()
42-
.setOptions(CelOptions.newBuilder().populateMacroCalls(true).build())
42+
.setOptions(
43+
CelOptions.newBuilder()
44+
.enableQuotedIdentifierSyntax(true)
45+
.populateMacroCalls(true)
46+
.build())
4347
.addLibraries(CelOptionalLibrary.INSTANCE)
4448
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
4549
.build();
@@ -99,6 +103,15 @@ public List<String> provideValues(Context context) {
99103
"a ? (b1 || b2) : (c1 && c2)",
100104
"(a ? b : c).method(d)",
101105
"a + b + c + d",
106+
"foo.`a.b`",
107+
"foo.`a/b`",
108+
"foo.`a-b`",
109+
"foo.`a b`",
110+
"foo.`in`",
111+
"Foo{`a.b`: foo}",
112+
"Foo{`a/b`: foo}",
113+
"Foo{`a-b`: foo}",
114+
"Foo{`a b`: foo}",
102115

103116
// Constants
104117
"true",
@@ -140,6 +153,7 @@ public List<String> provideValues(Context context) {
140153

141154
// Macros
142155
"has(x[\"a\"].single_int32)",
156+
"has(x.`foo-bar`.single_int32)",
143157

144158
// This is a filter expression but is decompiled back to
145159
// map(x, filter_function, x) for which the evaluation is

0 commit comments

Comments
 (0)