From cf3d719adf01afd7e0837d2a4e679ec8048f4172 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Tue, 12 Dec 2023 13:02:55 -0800 Subject: [PATCH] Add a string formatter for CelExpr PiperOrigin-RevId: 590307397 --- .../main/java/dev/cel/common/ast/BUILD.bazel | 1 + .../main/java/dev/cel/common/ast/CelExpr.java | 5 + .../dev/cel/common/ast/CelExprFormatter.java | 303 ++++++++++++++++ .../test/java/dev/cel/common/ast/BUILD.bazel | 2 + .../cel/common/ast/CelExprFormatterTest.java | 332 ++++++++++++++++++ 5 files changed, 643 insertions(+) create mode 100644 common/src/main/java/dev/cel/common/ast/CelExprFormatter.java create mode 100644 common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java diff --git a/common/src/main/java/dev/cel/common/ast/BUILD.bazel b/common/src/main/java/dev/cel/common/ast/BUILD.bazel index dc83cd2ac..c8663e6e3 100644 --- a/common/src/main/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/ast/BUILD.bazel @@ -11,6 +11,7 @@ package( AST_SOURCES = [ "CelConstant.java", "CelExpr.java", + "CelExprFormatter.java", "CelReference.java", ] diff --git a/common/src/main/java/dev/cel/common/ast/CelExpr.java b/common/src/main/java/dev/cel/common/ast/CelExpr.java index 9a93f547d..b075de5f3 100644 --- a/common/src/main/java/dev/cel/common/ast/CelExpr.java +++ b/common/src/main/java/dev/cel/common/ast/CelExpr.java @@ -1175,4 +1175,9 @@ public static CelExpr ofComprehension( .build())) .build(); } + + @Override + public final String toString() { + return CelExprFormatter.format(this); + } } diff --git a/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java b/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java new file mode 100644 index 000000000..4c8add1fa --- /dev/null +++ b/common/src/main/java/dev/cel/common/ast/CelExprFormatter.java @@ -0,0 +1,303 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.ast; + +/** Provides string formatting support for {@link CelExpr}. */ +final class CelExprFormatter { + private final StringBuilder indent = new StringBuilder(); + private final StringBuilder exprBuilder = new StringBuilder(); + + static String format(CelExpr celExpr) { + CelExprFormatter formatter = new CelExprFormatter(); + formatter.formatExpr(celExpr); + return formatter.exprBuilder.toString(); + } + + private void formatExpr(CelExpr celExpr) { + append(String.format("%s [%d] {", celExpr.exprKind().getKind(), celExpr.id())); + CelExpr.ExprKind.Kind exprKind = celExpr.exprKind().getKind(); + if (!exprKind.equals(CelExpr.ExprKind.Kind.CONSTANT)) { + appendNewline(); + } + + switch (exprKind) { + case CONSTANT: + appendConst(celExpr.constant()); + break; + case IDENT: + appendIdent(celExpr.ident()); + break; + case SELECT: + appendSelect(celExpr.select()); + break; + case CALL: + appendCall(celExpr.call()); + break; + case CREATE_LIST: + appendCreateList(celExpr.createList()); + break; + case CREATE_STRUCT: + appendCreateStruct(celExpr.createStruct()); + break; + case CREATE_MAP: + appendCreateMap(celExpr.createMap()); + break; + case COMPREHENSION: + appendComprehension(celExpr.comprehension()); + break; + default: + append("Unknown kind: " + exprKind); + break; + } + + if (!exprKind.equals(CelExpr.ExprKind.Kind.CONSTANT)) { + appendNewline(); + append("}"); + } else { + appendWithoutIndent("}"); + } + } + + private void appendConst(CelConstant celConstant) { + appendWithoutIndent(" value: "); + switch (celConstant.getKind()) { + case NULL_VALUE: + appendWithoutIndent("null"); + break; + case BOOLEAN_VALUE: + appendWithoutIndent(Boolean.toString(celConstant.booleanValue())); + break; + case INT64_VALUE: + appendWithoutIndent(Long.toString(celConstant.int64Value())); + break; + case UINT64_VALUE: + appendWithoutIndent(celConstant.uint64Value() + "u"); + break; + case DOUBLE_VALUE: + appendWithoutIndent(Double.toString(celConstant.doubleValue())); + break; + case STRING_VALUE: + appendWithoutIndent("\"" + celConstant.stringValue() + "\""); + break; + case BYTES_VALUE: + appendWithoutIndent(String.format("b\"%s\"", celConstant.bytesValue().toStringUtf8())); + break; + default: + append("Unknown kind: " + celConstant.getKind()); + break; + } + appendWithoutIndent(" "); + } + + private void appendIdent(CelExpr.CelIdent celIdent) { + indent(); + append("name: " + celIdent.name()); + outdent(); + } + + private void appendSelect(CelExpr.CelSelect celSelect) { + indent(); + formatExpr(celSelect.operand()); + outdent(); + append("."); + append(celSelect.field()); + if (celSelect.testOnly()) { + appendWithoutIndent("~presence_test"); + } + } + + private void appendCall(CelExpr.CelCall celCall) { + indent(); + appendWithNewline("function: " + celCall.function()); + if (celCall.target().isPresent()) { + appendWithNewline("target: {"); + indent(); + formatExpr(celCall.target().get()); + outdent(); + appendNewline(); + appendWithNewline("}"); + } + append("args: {"); + indent(); + for (CelExpr celExpr : celCall.args()) { + appendNewline(); + formatExpr(celExpr); + } + outdent(); + appendNewline(); + append("}"); + outdent(); + } + + private void appendCreateList(CelExpr.CelCreateList celCreateList) { + indent(); + append("elements: {"); + indent(); + for (CelExpr expr : celCreateList.elements()) { + appendNewline(); + formatExpr(expr); + } + outdent(); + appendNewline(); + append("}"); + if (!celCreateList.optionalIndices().isEmpty()) { + appendNewline(); + append("optional_indices: ["); + for (int i = 0; i < celCreateList.optionalIndices().size(); i++) { + appendWithoutIndent(String.valueOf(i)); + if (i != celCreateList.optionalIndices().size() - 1) { + appendWithoutIndent(", "); + } + } + appendWithoutIndent("]"); + } + outdent(); + } + + private void appendCreateStruct(CelExpr.CelCreateStruct celCreateStruct) { + indent(); + appendWithNewline("name: " + celCreateStruct.messageName()); + append("entries: {"); + indent(); + for (CelExpr.CelCreateStruct.Entry entry : celCreateStruct.entries()) { + appendNewline(); + appendWithNewline(String.format("ENTRY [%d] {", entry.id())); + indent(); + appendWithNewline("field_key: " + entry.fieldKey()); + if (entry.optionalEntry()) { + appendWithNewline("optional_entry: true"); + } + appendWithNewline("value: {"); + indent(); + formatExpr(entry.value()); + outdent(); + appendNewline(); + appendWithNewline("}"); + outdent(); + append("}"); + } + outdent(); + appendNewline(); + append("}"); + outdent(); + } + + private void appendCreateMap(CelExpr.CelCreateMap celCreateMap) { + indent(); + boolean firstLine = true; + for (CelExpr.CelCreateMap.Entry entry : celCreateMap.entries()) { + if (!firstLine) { + appendNewline(); + } else { + firstLine = false; + } + appendWithNewline(String.format("MAP_ENTRY [%d] {", entry.id())); + indent(); + appendWithNewline("key: {"); + indent(); + formatExpr(entry.key()); + outdent(); + appendNewline(); + appendWithNewline("}"); + if (entry.optionalEntry()) { + appendWithNewline("optional_entry: true"); + } + appendWithNewline("value: {"); + indent(); + formatExpr(entry.value()); + outdent(); + appendNewline(); + appendWithNewline("}"); + outdent(); + append("}"); + } + outdent(); + } + + private void appendComprehension(CelExpr.CelComprehension celComprehension) { + indent(); + appendWithNewline("iter_var: " + celComprehension.iterVar()); + // Iter range + appendWithNewline("iter_range: {"); + indent(); + formatExpr(celComprehension.iterRange()); + outdent(); + appendNewline(); + appendWithNewline("}"); + + appendWithNewline("accu_var: " + celComprehension.accuVar()); + // Accu init + appendWithNewline("accu_init: {"); + indent(); + formatExpr(celComprehension.accuInit()); + outdent(); + appendNewline(); + appendWithNewline("}"); + + // Loop condition + appendWithNewline("loop_condition: {"); + indent(); + formatExpr(celComprehension.loopCondition()); + outdent(); + appendNewline(); + appendWithNewline("}"); + + // Loop step + appendWithNewline("loop_step: {"); + indent(); + formatExpr(celComprehension.loopStep()); + outdent(); + appendNewline(); + appendWithNewline("}"); + + // Result + appendWithNewline("result: {"); + indent(); + formatExpr(celComprehension.result()); + outdent(); + appendNewline(); + append("}"); + + outdent(); + } + + private void append(String str) { + exprBuilder.append(indent); + exprBuilder.append(str); + } + + private void appendWithNewline(String str) { + append(str); + appendNewline(); + } + + private void appendWithoutIndent(String str) { + exprBuilder.append(str); + } + + private void appendNewline() { + exprBuilder.append('\n'); + } + + private void indent() { + indent.append(" "); + } + + private void outdent() { + indent.setLength(indent.length() - 2); + } + + private CelExprFormatter() {} +} diff --git a/common/src/test/java/dev/cel/common/ast/BUILD.bazel b/common/src/test/java/dev/cel/common/ast/BUILD.bazel index 0e22c429f..595d516ee 100644 --- a/common/src/test/java/dev/cel/common/ast/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/ast/BUILD.bazel @@ -12,6 +12,7 @@ java_library( "//:auto_value", "//:java_truth", "//common", + "//common:compiler_common", "//common/ast", "//common/ast:cel_expr_visitor", "//common/ast:expr_converter", @@ -21,6 +22,7 @@ java_library( "//common/types", "//compiler", "//compiler:compiler_builder", + "//extensions:optional_library", "//parser:macro", "//parser:operator", "@cel_spec//proto/cel/expr:expr_java_proto", diff --git a/common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java b/common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java new file mode 100644 index 000000000..b80779bf7 --- /dev/null +++ b/common/src/test/java/dev/cel/common/ast/CelExprFormatterTest.java @@ -0,0 +1,332 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.ast; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.parser.CelStandardMacro; +import dev.cel.testing.testdata.proto3.TestAllTypesProto.TestAllTypes; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelExprFormatterTest { + + private enum ConstantTestCase { + BOOL("true", "CONSTANT [1] { value: true }"), + INT("123", "CONSTANT [1] { value: 123 }"), + UINT("123u", "CONSTANT [1] { value: 123u }"), + DOUBLE("3.5", "CONSTANT [1] { value: 3.5 }"), + BYTES("b'abc'", "CONSTANT [1] { value: b\"abc\" }"), + STRING("'hello world'", "CONSTANT [1] { value: \"hello world\" }"); + + private final String expression; + private final String formatted; + + ConstantTestCase(String expression, String formatted) { + this.expression = expression; + this.formatted = formatted; + } + } + + @Test + public void constant(@TestParameter ConstantTestCase constantTestCase) throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + CelAbstractSyntaxTree ast = celCompiler.compile(constantTestCase.expression).getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr).isEqualTo(constantTestCase.formatted); + } + + @Test + public void select() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())) + .build(); + CelAbstractSyntaxTree ast = celCompiler.compile("msg.single_int32").getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr) + .isEqualTo("SELECT [2] {\n IDENT [1] {\n name: msg\n }.single_int32\n}"); + } + + @Test + public void call_global() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "test", + CelOverloadDecl.newGlobalOverload( + "test_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = celCompiler.compile("test(1, 5)").getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr) + .isEqualTo( + "CALL [1] {\n" + + " function: test\n" + + " args: {\n" + + " CONSTANT [2] { value: 1 }\n" + + " CONSTANT [3] { value: 5 }\n" + + " }\n" + + "}"); + } + + @Test + public void call_member() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "test", + CelOverloadDecl.newMemberOverload( + "test_overload", SimpleType.INT, SimpleType.INT, SimpleType.INT))) + .build(); + CelAbstractSyntaxTree ast = celCompiler.compile("5.test(2)").getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr) + .isEqualTo( + "CALL [2] {\n" + + " function: test\n" + + " target: {\n" + + " CONSTANT [1] { value: 5 }\n" + + " }\n" + + " args: {\n" + + " CONSTANT [3] { value: 2 }\n" + + " }\n" + + "}"); + } + + @Test + public void create_list() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addLibraries(CelOptionalLibrary.INSTANCE) + .build(); + CelAbstractSyntaxTree ast = + celCompiler.compile("[1, 2, ?optional.of(3), ?optional.of(4)]").getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr) + .isEqualTo( + "CREATE_LIST [1] {\n" + + " elements: {\n" + + " CONSTANT [2] { value: 1 }\n" + + " CONSTANT [3] { value: 2 }\n" + + " CALL [5] {\n" + + " function: optional.of\n" + + " args: {\n" + + " CONSTANT [6] { value: 3 }\n" + + " }\n" + + " }\n" + + " CALL [8] {\n" + + " function: optional.of\n" + + " args: {\n" + + " CONSTANT [9] { value: 4 }\n" + + " }\n" + + " }\n" + + " }\n" + + " optional_indices: [0, 1]\n" + + "}"); + } + + @Test + public void create_struct() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer("dev.cel.testing.testdata.proto3") + .addMessageTypes(TestAllTypes.getDescriptor()) + .addLibraries(CelOptionalLibrary.INSTANCE) + .build(); + CelAbstractSyntaxTree ast = + celCompiler + .compile( + "TestAllTypes{single_int64: 1, single_string: 'test', ?single_double:" + + " optional.of(5.0)}") + .getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr) + .isEqualTo( + "CREATE_STRUCT [1] {\n" + + " name: TestAllTypes\n" + + " entries: {\n" + + " ENTRY [2] {\n" + + " field_key: single_int64\n" + + " value: {\n" + + " CONSTANT [3] { value: 1 }\n" + + " }\n" + + " }\n" + + " ENTRY [4] {\n" + + " field_key: single_string\n" + + " value: {\n" + + " CONSTANT [5] { value: \"test\" }\n" + + " }\n" + + " }\n" + + " ENTRY [6] {\n" + + " field_key: single_double\n" + + " optional_entry: true\n" + + " value: {\n" + + " CALL [8] {\n" + + " function: optional.of\n" + + " args: {\n" + + " CONSTANT [9] { value: 5.0 }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + } + + @Test + public void create_map() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setContainer("dev.cel.testing.testdata.proto3") + .addMessageTypes(TestAllTypes.getDescriptor()) + .addLibraries(CelOptionalLibrary.INSTANCE) + .build(); + CelAbstractSyntaxTree ast = + celCompiler.compile("{1: 'a', 'test': true, ?false: optional.of(3.0)}").getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr) + .isEqualTo( + "CREATE_MAP [1] {\n" + + " MAP_ENTRY [2] {\n" + + " key: {\n" + + " CONSTANT [3] { value: 1 }\n" + + " }\n" + + " value: {\n" + + " CONSTANT [4] { value: \"a\" }\n" + + " }\n" + + " }\n" + + " MAP_ENTRY [5] {\n" + + " key: {\n" + + " CONSTANT [6] { value: \"test\" }\n" + + " }\n" + + " value: {\n" + + " CONSTANT [7] { value: true }\n" + + " }\n" + + " }\n" + + " MAP_ENTRY [8] {\n" + + " key: {\n" + + " CONSTANT [9] { value: false }\n" + + " }\n" + + " optional_entry: true\n" + + " value: {\n" + + " CALL [11] {\n" + + " function: optional.of\n" + + " args: {\n" + + " CONSTANT [12] { value: 3.0 }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + } + + @Test + public void comprehension() throws Exception { + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .build(); + CelAbstractSyntaxTree ast = celCompiler.compile("[1, 2, 3].exists(x, x > 0)").getAst(); + + String formattedExpr = CelExprFormatter.format(ast.getExpr()); + + assertThat(formattedExpr) + .isEqualTo( + "COMPREHENSION [17] {\n" + + " iter_var: x\n" + + " iter_range: {\n" + + " CREATE_LIST [1] {\n" + + " elements: {\n" + + " CONSTANT [2] { value: 1 }\n" + + " CONSTANT [3] { value: 2 }\n" + + " CONSTANT [4] { value: 3 }\n" + + " }\n" + + " }\n" + + " }\n" + + " accu_var: __result__\n" + + " accu_init: {\n" + + " CONSTANT [10] { value: false }\n" + + " }\n" + + " loop_condition: {\n" + + " CALL [13] {\n" + + " function: @not_strictly_false\n" + + " args: {\n" + + " CALL [12] {\n" + + " function: !_\n" + + " args: {\n" + + " IDENT [11] {\n" + + " name: __result__\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " loop_step: {\n" + + " CALL [15] {\n" + + " function: _||_\n" + + " args: {\n" + + " IDENT [14] {\n" + + " name: __result__\n" + + " }\n" + + " CALL [8] {\n" + + " function: _>_\n" + + " args: {\n" + + " IDENT [7] {\n" + + " name: x\n" + + " }\n" + + " CONSTANT [9] { value: 0 }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " result: {\n" + + " IDENT [16] {\n" + + " name: __result__\n" + + " }\n" + + " }\n" + + "}"); + } +}