Skip to content

Commit 712b265

Browse files
l46kokcopybara-github
authored andcommitted
Add abbreviation support to type-checker
PiperOrigin-RevId: 788101544
1 parent c90e451 commit 712b265

8 files changed

Lines changed: 266 additions & 18 deletions

File tree

checker/src/test/java/dev/cel/checker/ExprCheckerTest.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -984,15 +984,7 @@ public void optionalErrors() throws Exception {
984984
CelAbstractSyntaxTree parsedAst = celCompiler.parse(source).getAst();
985985
CelMutableAst mutableAst = CelMutableAst.fromCelAst(parsedAst);
986986
mutableAst.expr().call().args().get(1).setConstant(CelConstant.ofValue(true));
987-
// ParsedExpr parsedExpr =
988-
// CelProtoAbstractSyntaxTree.fromCelAst(celCompiler.parse(source).getAst()).toParsedExpr();
989-
// ParsedExpr.Builder parsedExprBuilder = parsedExpr.toBuilder();
990-
// parsedExprBuilder
991-
// .getExprBuilder()
992-
// .getCallExprBuilder()
993-
// .getArgsBuilder(1)
994-
// .setConstExpr(Constant.newBuilder().setBoolValue(true).build()); // Const must be a
995-
// string
987+
996988
runErroneousTest(mutableAst.toParsedAst());
997989
}
998990

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

Lines changed: 142 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
package dev.cel.common;
1616

1717
import com.google.auto.value.AutoValue;
18+
import com.google.common.base.Preconditions;
1819
import com.google.common.collect.ImmutableMap;
1920
import com.google.common.collect.ImmutableSet;
2021
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2122
import com.google.errorprone.annotations.CheckReturnValue;
2223
import com.google.errorprone.annotations.Immutable;
2324
import java.util.LinkedHashMap;
25+
import java.util.Locale;
2426
import java.util.Optional;
2527

2628
/** CelContainer holds a reference to an optional qualified container name and set of aliases. */
@@ -38,20 +40,125 @@ public abstract static class Builder {
3840

3941
private final LinkedHashMap<String, String> aliases = new LinkedHashMap<>();
4042

43+
abstract String name();
44+
4145
/** Sets the fully-qualified name of the container. */
4246
public abstract Builder setName(String name);
4347

4448
abstract Builder setAliases(ImmutableMap<String, String> aliases);
4549

46-
/** Alias associates a fully-qualified name with a user-defined alias. */
50+
/** See {@link #addAbbreviations(ImmutableSet)} for documentation. */
51+
@CanIgnoreReturnValue
52+
public Builder addAbbreviations(String... qualifiedNames) {
53+
Preconditions.checkNotNull(qualifiedNames);
54+
return addAbbreviations(ImmutableSet.copyOf(qualifiedNames));
55+
}
56+
57+
/**
58+
* Configures a set of simple names as abbreviations for fully-qualified names.
59+
*
60+
* <p>An abbreviation is a simple name that expands to a fully-qualified name. Abbreviations can
61+
* be useful when working with variables, functions, and especially types from multiple
62+
* namespaces:
63+
*
64+
* <pre>{@code
65+
* // CEL object construction
66+
* qual.pkg.version.ObjTypeName{
67+
* field: alt.container.ver.FieldTypeName{value: ...}
68+
* }
69+
* }</pre>
70+
*
71+
* <p>Only one the qualified names above may be used as the CEL container, so at least one of
72+
* these references must be a long qualified name within an otherwise short CEL program. Using
73+
* the following abbreviations, the program becomes much simpler:
74+
*
75+
* <pre>{@code
76+
* // CEL Java option
77+
* CelContainer.newBuilder().addAbbreviations("qual.pkg.version.ObjTypeName", "alt.container.ver.FieldTypeName").build()
78+
* }
79+
* {@code
80+
* // Simplified Object construction
81+
* ObjTypeName{field: FieldTypeName{value: ...}}
82+
* }</pre>
83+
*
84+
* <p>There are a few rules for the qualified names and the simple abbreviations generated from
85+
* them:
86+
*
87+
* <ul>
88+
* <li>Qualified names must be dot-delimited, e.g. `package.subpkg.name`.
89+
* <li>The last element in the qualified name is the abbreviation.
90+
* <li>Abbreviations must not collide with each other.
91+
* <li>The abbreviation must not collide with unqualified names in use.
92+
* </ul>
93+
*
94+
* <p>Abbreviations are distinct from container-based references in the following important
95+
* ways:
96+
*
97+
* <ul>
98+
* <li>Abbreviations must expand to a fully-qualified name.
99+
* <li>Expanded abbreviations do not participate in namespace resolution.
100+
* <li>Abbreviation expansion is done instead of the container search for a matching
101+
* identifier.
102+
* <li>Containers follow C++ namespace resolution rules with searches from the most qualified
103+
* name to the least qualified name.
104+
* <li>Container references within the CEL program may be relative, and are resolved to fully
105+
* qualified names at either type-check time or program plan time, whichever comes first.
106+
* </ul>
107+
*
108+
* <p>If there is ever a case where an identifier could be in both the container and as an
109+
* abbreviation, the abbreviation wins as this will ensure that the meaning of a program is
110+
* preserved between compilations even as the container evolves.
111+
*/
112+
@CanIgnoreReturnValue
113+
public Builder addAbbreviations(ImmutableSet<String> qualifiedNames) {
114+
for (String qualifiedName : qualifiedNames) {
115+
qualifiedName = qualifiedName.trim();
116+
for (int i = 0; i < qualifiedName.length(); i++) {
117+
if (!isIdentifierChar(qualifiedName.charAt(i))) {
118+
throw new IllegalArgumentException(
119+
String.format(
120+
"invalid qualified name: %s, wanted name of the form 'qualified.name'",
121+
qualifiedName));
122+
}
123+
}
124+
125+
int index = qualifiedName.lastIndexOf(".");
126+
if (index <= 0 || index >= qualifiedName.length() - 1) {
127+
throw new IllegalArgumentException(
128+
String.format(
129+
"invalid qualified name: %s, wanted name of the form 'qualified.name'",
130+
qualifiedName));
131+
}
132+
133+
String alias = qualifiedName.substring(index + 1);
134+
aliasAs(AliasKind.ABBREVIATION, qualifiedName, alias);
135+
}
136+
137+
return this;
138+
}
139+
140+
/**
141+
* Alias associates a fully-qualified name with a user-defined alias.
142+
*
143+
* <p>In general, {@link #addAbbreviations} is preferred to aliasing since the names generated
144+
* from the Abbrevs option are more easily traced back to source code. Aliasing is useful for
145+
* propagating alias configuration from one container instance to another, and may also be
146+
* useful for remapping poorly chosen protobuf message / package names.
147+
*
148+
* <p>Note: all the rules that apply to abbreviations also apply to aliasing.
149+
*/
47150
@CanIgnoreReturnValue
48151
public Builder addAlias(String alias, String qualifiedName) {
49-
validateAliasOrThrow("alias", qualifiedName, alias);
50-
aliases.put(alias, qualifiedName);
152+
aliasAs(AliasKind.ALIAS, qualifiedName, alias);
51153
return this;
52154
}
53155

54-
private void validateAliasOrThrow(String kind, String qualifiedName, String alias) {
156+
private void aliasAs(AliasKind kind, String qualifiedName, String alias) {
157+
validateAliasOrThrow(kind, qualifiedName, alias);
158+
aliases.put(alias, qualifiedName);
159+
}
160+
161+
private void validateAliasOrThrow(AliasKind kind, String qualifiedName, String alias) {
55162
if (alias.isEmpty() || alias.contains(".")) {
56163
throw new IllegalArgumentException(
57164
String.format(
@@ -76,6 +183,14 @@ private void validateAliasOrThrow(String kind, String qualifiedName, String alia
76183
"%s collides with existing reference: name=%s, %s=%s, existing=%s",
77184
kind, qualifiedName, kind, alias, aliasRef));
78185
}
186+
187+
String containerName = name();
188+
if (containerName.startsWith(alias + ".") || containerName.equals(alias)) {
189+
throw new IllegalArgumentException(
190+
String.format(
191+
"%s collides with container name: name=%s, %s=%s, container=%s",
192+
kind, qualifiedName, kind, alias, containerName));
193+
}
79194
}
80195

81196
abstract CelContainer autoBuild();
@@ -135,6 +250,14 @@ public ImmutableSet<String> resolveCandidateNames(String typeName) {
135250
return candidates.add(typeName).build();
136251
}
137252

253+
public static Builder newBuilder() {
254+
return new AutoValue_CelContainer.Builder().setName("");
255+
}
256+
257+
public static CelContainer ofName(String containerName) {
258+
return newBuilder().setName(containerName).build();
259+
}
260+
138261
private Optional<String> findAlias(String name) {
139262
// If an alias exists for the name, ensure it is searched last.
140263
String simple = name;
@@ -152,11 +275,22 @@ private Optional<String> findAlias(String name) {
152275
return Optional.of(alias + qualifier);
153276
}
154277

155-
public static Builder newBuilder() {
156-
return new AutoValue_CelContainer.Builder().setName("");
278+
private static boolean isIdentifierChar(int r) {
279+
if (r > 127) {
280+
// Not ASCII
281+
return false;
282+
}
283+
284+
return r == '.' || r == '_' || Character.isLetter(r) || Character.isDigit(r);
157285
}
158286

159-
public static CelContainer ofName(String containerName) {
160-
return newBuilder().setName(containerName).build();
287+
private enum AliasKind {
288+
ALIAS,
289+
ABBREVIATION;
290+
291+
@Override
292+
public String toString() {
293+
return this.name().toLowerCase(Locale.getDefault());
294+
}
161295
}
162296
}

common/src/test/java/dev/cel/common/CelContainerTest.java

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,30 @@ public void resolveCandidateName_withAlias_resolvesSingleCandidate(
6666
}
6767

6868
@Test
69-
public void containerBuilder_aliasCollides_throws() {
69+
@TestParameters("{typeName: R, resolved: my.alias.R}")
70+
@TestParameters("{typeName: R.S.T, resolved: my.alias.R.S.T}")
71+
public void resolveCandidateName_withMatchingAbbreviation_resolvesSingleCandidate(
72+
String typeName, String resolved) {
73+
CelContainer container =
74+
CelContainer.newBuilder().setName("a.b.c").addAbbreviations("my.alias.R").build();
75+
76+
ImmutableSet<String> resolvedNames = container.resolveCandidateNames(typeName);
77+
78+
assertThat(resolvedNames).containsExactly(resolved);
79+
}
80+
81+
@Test
82+
public void resolveCandidateName_withUnmatchedAbbreviation_resolvesMultipleCandidates() {
83+
CelContainer container =
84+
CelContainer.newBuilder().setName("a.b.c").addAbbreviations("my.alias.R").build();
85+
86+
ImmutableSet<String> resolvedNames = container.resolveCandidateNames("S");
87+
88+
assertThat(resolvedNames).containsExactly("a.b.c.S", "a.b.S", "a.S", "S").inOrder();
89+
}
90+
91+
@Test
92+
public void containerBuilder_duplicateAliases_throws() {
7093
IllegalArgumentException e =
7194
assertThrows(
7295
IllegalArgumentException.class,
@@ -76,6 +99,17 @@ public void containerBuilder_aliasCollides_throws() {
7699
.contains("alias collides with existing reference: name=b.c, alias=foo, existing=a.b");
77100
}
78101

102+
@Test
103+
public void containerBuilder_aliasCollidesWithContainer_throws() {
104+
IllegalArgumentException e =
105+
assertThrows(
106+
IllegalArgumentException.class,
107+
() -> CelContainer.newBuilder().setName("foo").addAlias("foo", "a.b"));
108+
assertThat(e)
109+
.hasMessageThat()
110+
.contains("alias collides with container name: name=a.b, alias=foo, container=foo");
111+
}
112+
79113
@Test
80114
public void containerBuilder_addAliasError_throws(@TestParameter AliasingErrorTestCase testCase) {
81115
IllegalArgumentException e =
@@ -106,4 +140,62 @@ private enum AliasingErrorTestCase {
106140
this.errorMessage = errorMessage;
107141
}
108142
}
143+
144+
@Test
145+
public void containerBuilder_addAbbreviationsError_throws(
146+
@TestParameter AbbreviationErrorTestCase testCase) {
147+
IllegalArgumentException e =
148+
assertThrows(
149+
IllegalArgumentException.class,
150+
() -> CelContainer.newBuilder().addAbbreviations(testCase.qualifiedNames));
151+
assertThat(e).hasMessageThat().contains(testCase.errorMessage);
152+
}
153+
154+
private enum AbbreviationErrorTestCase {
155+
ABBREVIATION_COLLISION(
156+
ImmutableSet.of("my.alias.R", "yer.other.R"),
157+
"abbreviation collides with existing reference: name=yer.other.R, abbreviation=R,"
158+
+ " existing=my.alias.R"),
159+
INVALID_DOT_PREFIX(
160+
".bad", "invalid qualified name: .bad, wanted name of the form 'qualified.name'"),
161+
INVALID_DOT_SUFFIX(
162+
"bad.alias.",
163+
"invalid qualified name: bad.alias., wanted name of the form 'qualified.name'"),
164+
NO_QUALIFIER(
165+
" bad_alias1 ",
166+
"invalid qualified name: bad_alias1, wanted name of the form 'qualified.name'"),
167+
INVALID_IDENTIFIER(
168+
" bad.alias!",
169+
"invalid qualified name: bad.alias!, wanted name of the form 'qualified.name'"),
170+
;
171+
172+
private final ImmutableSet<String> qualifiedNames;
173+
private final String errorMessage;
174+
175+
AbbreviationErrorTestCase(String qualifiedNames, String errorMessage) {
176+
this(ImmutableSet.of(qualifiedNames), errorMessage);
177+
}
178+
179+
AbbreviationErrorTestCase(ImmutableSet<String> qualifiedNames, String errorMessage) {
180+
this.qualifiedNames = qualifiedNames;
181+
this.errorMessage = errorMessage;
182+
}
183+
}
184+
185+
@Test
186+
public void containerBuilder_addAbbreviationsCollidesWithContainer_throws() {
187+
IllegalArgumentException e =
188+
assertThrows(
189+
IllegalArgumentException.class,
190+
() ->
191+
CelContainer.newBuilder()
192+
.setName("a.b.c.M.N")
193+
.addAbbreviations("my.alias.a", "yer.other.b"));
194+
195+
assertThat(e)
196+
.hasMessageThat()
197+
.contains(
198+
"abbreviation collides with container name: name=my.alias.a, abbreviation=a,"
199+
+ " container=a.b.c.M.N");
200+
}
109201
}

runtime/src/test/java/dev/cel/runtime/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ java_library(
173173
"//extensions:optional_library",
174174
"//runtime",
175175
"//testing:base_interpreter_test",
176+
"//testing/protos:test_all_types_cel_java_proto2",
176177
"//testing/protos:test_all_types_cel_java_proto3",
177178
"@maven//:com_google_testparameterinjector_test_parameter_injector",
178179
"@maven//:junit_junit",

runtime/src/test/java/dev/cel/runtime/CelLiteInterpreterTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public CelLiteInterpreterTest() {
3232
CelRuntimeFactory.standardCelRuntimeBuilder()
3333
.setValueProvider(
3434
ProtoMessageLiteValueProvider.newInstance(
35+
dev.cel.expr.conformance.proto2.TestAllTypesCelDescriptor.getDescriptor(),
3536
TestAllTypesCelDescriptor.getDescriptor()))
3637
.addLibraries(CelOptionalLibrary.INSTANCE)
3738
.setOptions(newBaseCelOptions().toBuilder().enableCelValue(true).build())

runtime/src/test/resources/containers.baseline

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,18 @@ Source: test_alias.TestAllTypes{} == cel.expr.conformance.proto3.TestAllTypes{}
22
=====>
33
bindings: {}
44
result: true
5+
6+
Source: proto2.TestAllTypes{} == cel.expr.conformance.proto2.TestAllTypes{}
7+
=====>
8+
bindings: {}
9+
result: true
10+
11+
Source: proto3.TestAllTypes{} == cel.expr.conformance.proto3.TestAllTypes{}
12+
=====>
13+
bindings: {}
14+
result: true
15+
16+
Source: SGAR
17+
=====>
18+
bindings: {}
19+
result: 1

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ java_library(
9090
"//runtime:late_function_binding",
9191
"//runtime:unknown_attributes",
9292
"@cel_spec//proto/cel/expr:checked_java_proto",
93+
"@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto",
9394
"@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto",
9495
"@maven//:com_google_errorprone_error_prone_annotations",
9596
"@maven//:com_google_guava_guava",

0 commit comments

Comments
 (0)