From c727f26ad6aee96e81fe5cd6b52e803dbfad9dfd Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Fri, 16 Oct 2026 15:50:51 -0700 Subject: [PATCH] Add canonical CEL signature generation utilities and automatic overload ID population. - Introduce internal `CelSignatureUtils` in `dev.cel.common` for generating canonical CEL type and function overload signatures (`makeTypeSignature`, `makeOverloadSignature`). - Make `overloadId` optional on `CelOverloadDecl` (defaulting to `""` in `newBuilder()` and adding `overloadId`-less `newGlobalOverload` and `newMemberOverload` factories) and automatically populate empty overload IDs in `CelFunctionDecl.Builder.build()`. PiperOrigin-RevId: 1001151765 --- .../dev/cel/checker/CelOverloadDeclTest.java | 40 +++ common/BUILD.bazel | 12 + .../src/main/java/dev/cel/common/BUILD.bazel | 28 ++ .../java/dev/cel/common/CelFunctionDecl.java | 48 +++- .../java/dev/cel/common/CelOverloadDecl.java | 79 +++++- .../dev/cel/common/CelSignatureUtils.java | 249 ++++++++++++++++++ .../src/test/java/dev/cel/common/BUILD.bazel | 4 + .../dev/cel/common/CelSignatureUtilsTest.java | 163 ++++++++++++ 8 files changed, 619 insertions(+), 4 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/CelSignatureUtils.java create mode 100644 common/src/test/java/dev/cel/common/CelSignatureUtilsTest.java diff --git a/checker/src/test/java/dev/cel/checker/CelOverloadDeclTest.java b/checker/src/test/java/dev/cel/checker/CelOverloadDeclTest.java index 4e851a56f..5a3e07bbc 100644 --- a/checker/src/test/java/dev/cel/checker/CelOverloadDeclTest.java +++ b/checker/src/test/java/dev/cel/checker/CelOverloadDeclTest.java @@ -21,9 +21,11 @@ import dev.cel.expr.Decl.FunctionDecl.Overload; import com.google.common.collect.ImmutableList; +import dev.cel.common.CelFunctionDecl; import dev.cel.common.CelOverloadDecl; import dev.cel.common.CelProtoDeclConverter; import dev.cel.common.types.CelProtoTypes; +import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeParamType; import org.junit.Test; @@ -105,4 +107,42 @@ public void setParameterTypes_doesNotDedupe() { .containsExactly(SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.INT) .inOrder(); } + + @Test + public void newFunctionDeclaration_populatesEmptyOverloadIds() { + CelOverloadDecl globalOverload = + newGlobalOverload(SimpleType.INT, SimpleType.INT, SimpleType.INT); + CelOverloadDecl globalListOverload = + newGlobalOverload(SimpleType.STRING, ImmutableList.of(SimpleType.INT)); + CelOverloadDecl memberOverload = + newMemberOverload( + SimpleType.BOOL, ListType.create(TypeParamType.create("T")), TypeParamType.create("T")); + CelOverloadDecl memberListOverload = + newMemberOverload(SimpleType.INT, ImmutableList.of(SimpleType.STRING)); + CelOverloadDecl explicitOverload = + newGlobalOverload("custom_id", SimpleType.STRING, SimpleType.STRING); + + assertThat(globalOverload.overloadId()).isEmpty(); + assertThat(globalListOverload.overloadId()).isEmpty(); + assertThat(memberOverload.overloadId()).isEmpty(); + assertThat(memberListOverload.overloadId()).isEmpty(); + + CelFunctionDecl functionDecl = + CelFunctionDecl.newFunctionDeclaration( + "myFunc", + globalOverload, + globalListOverload, + memberOverload, + memberListOverload, + explicitOverload); + + assertThat(functionDecl.overloads().stream().map(CelOverloadDecl::overloadId)) + .containsExactly( + "myFunc(int,int)", + "myFunc(int)", + "list<~T>.myFunc(~T)", + "string.myFunc()", + "custom_id") + .inOrder(); + } } diff --git a/common/BUILD.bazel b/common/BUILD.bazel index c84701d17..e609ff6e7 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -40,6 +40,18 @@ cel_android_library( exports = ["//common/src/main/java/dev/cel/common:cel_overload_decl_android"], ) +java_library( + name = "cel_signature_utils", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common:cel_signature_utils"], +) + +cel_android_library( + name = "cel_signature_utils_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common:cel_signature_utils_android"], +) + java_library( name = "cel_proto_decl_converter", exports = ["//common/src/main/java/dev/cel/common:cel_proto_decl_converter"], diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index cb9487b8c..77054cb81 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -63,6 +63,7 @@ java_library( ], deps = [ ":cel_overload_decl", + ":cel_signature_utils", "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -76,12 +77,39 @@ cel_android_library( ], deps = [ ":cel_overload_decl_android", + ":cel_signature_utils_android", "//:auto_value", "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", ], ) +java_library( + name = "cel_signature_utils", + srcs = ["CelSignatureUtils.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/types", + "//common/types:type_providers", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "cel_signature_utils_android", + srcs = ["CelSignatureUtils.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "//common/types:type_providers_android", + "//common/types:types_android", + "@maven_android//:com_google_guava_guava", + ], +) + java_library( name = "cel_overload_decl", srcs = ["CelOverloadDecl.java"], diff --git a/common/src/main/java/dev/cel/common/CelFunctionDecl.java b/common/src/main/java/dev/cel/common/CelFunctionDecl.java index c8cc79908..37bdc5941 100644 --- a/common/src/main/java/dev/cel/common/CelFunctionDecl.java +++ b/common/src/main/java/dev/cel/common/CelFunctionDecl.java @@ -14,6 +14,7 @@ package dev.cel.common; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; @@ -22,6 +23,8 @@ import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; /** Abstract representation of a CEL Function declaration. */ @AutoValue @@ -70,9 +73,50 @@ public Builder addOverloads(Iterable overloads) { return this; } - /** Builds a new instance of {@link CelFunctionDecl} */ @CheckReturnValue - public abstract CelFunctionDecl build(); + abstract CelFunctionDecl autoBuild(); + + /** + * Builds a new instance of {@link CelFunctionDecl}. + * + *

Any {@link CelOverloadDecl} with an empty {@link CelOverloadDecl#overloadId()} will have + * its ID automatically populated with its canonical overload signature. + */ + @CheckReturnValue + public CelFunctionDecl build() { + ImmutableSet currentOverloads = overloads(); + boolean needsOverloadId = false; + for (CelOverloadDecl overload : currentOverloads) { + if (overload.overloadId().isEmpty()) { + needsOverloadId = true; + break; + } + } + if (!needsOverloadId) { + return autoBuild(); + } + String functionName = name(); + ImmutableSet.Builder updatedOverloads = + ImmutableSet.builderWithExpectedSize(currentOverloads.size()); + Set seenOverloadIds = new HashSet<>(); + for (CelOverloadDecl overload : currentOverloads) { + String overloadId = overload.overloadId(); + if (overloadId.isEmpty()) { + overloadId = + CelSignatureUtils.makeOverloadSignature( + functionName, overload.parameterTypes(), overload.isInstanceFunction()); + updatedOverloads.add(overload.toBuilder().setOverloadId(overloadId).build()); + } else { + updatedOverloads.add(overload); + } + checkArgument( + seenOverloadIds.add(overloadId), "Duplicate overload signature: %s", overloadId); + } + return new AutoValue_CelFunctionDecl.Builder() + .setName(functionName) + .setOverloads(updatedOverloads.build()) + .autoBuild(); + } } public abstract Builder toBuilder(); diff --git a/common/src/main/java/dev/cel/common/CelOverloadDecl.java b/common/src/main/java/dev/cel/common/CelOverloadDecl.java index f17dd87b1..777281359 100644 --- a/common/src/main/java/dev/cel/common/CelOverloadDecl.java +++ b/common/src/main/java/dev/cel/common/CelOverloadDecl.java @@ -41,7 +41,10 @@ @AutoValue @Immutable public abstract class CelOverloadDecl { - /** Required. Globally unique overload name. */ + /** + * Globally unique overload name. If empty when building {@link CelFunctionDecl}, an overload ID + * is automatically generated from the function name and parameter types. + */ public abstract String overloadId(); /** @@ -79,7 +82,7 @@ public abstract class CelOverloadDecl { /** Create a new builder to construct a {@link CelOverloadDecl} instance */ public static Builder newBuilder() { - return new AutoValue_CelOverloadDecl.Builder().setDoc(""); + return new AutoValue_CelOverloadDecl.Builder().setOverloadId("").setDoc(""); } /** Builder for configuring the {@link CelOverloadDecl}. */ @@ -153,6 +156,44 @@ public CelOverloadDecl build() { } } + /** + * Helper method for declaring a member function overload (e.g. {@code x.f(y, ...)}) with an + * automatically generated overload ID. + * + *

The returned declaration has an empty {@link #overloadId()}, which is automatically + * populated with the canonical signature (e.g. {@code "target.func(arg1,arg2)"}) when built into + * a {@link CelFunctionDecl} via {@link CelFunctionDecl.Builder#build()} or {@link + * CelFunctionDecl#newFunctionDeclaration}. + * + * @param resultType the return type of the overload + * @param paramTypes the parameter types of the overload, where the first element ({@code + * paramTypes[0]}) is the receiver (target) type {@code x} and any subsequent elements are the + * method argument types {@code y, ...} + */ + @CheckReturnValue + public static CelOverloadDecl newMemberOverload(CelType resultType, CelType... paramTypes) { + return newMemberOverload("", resultType, Arrays.asList(paramTypes)); + } + + /** + * Helper method for declaring a member function overload (e.g. {@code x.f(y, ...)}) with an + * automatically generated overload ID. + * + *

The returned declaration has an empty {@link #overloadId()}, which is automatically + * populated with the canonical signature (e.g. {@code "target.func(arg1,arg2)"}) when built into + * a {@link CelFunctionDecl} via {@link CelFunctionDecl.Builder#build()} or {@link + * CelFunctionDecl#newFunctionDeclaration}. + * + * @param resultType the return type of the overload + * @param paramTypes the parameter types of the overload, where the first element ({@code + * paramTypes.get(0)}) is the receiver (target) type {@code x} and any subsequent elements are + * the method argument types {@code y, ...} + */ + @CheckReturnValue + public static CelOverloadDecl newMemberOverload(CelType resultType, List paramTypes) { + return newMemberOverload("", /* doc= */ "", resultType, paramTypes); + } + /** Helper method for declaring a member function overload */ @CheckReturnValue public static CelOverloadDecl newMemberOverload( @@ -181,6 +222,40 @@ public static CelOverloadDecl newMemberOverload( return newOverload(overloadId, doc, resultType, paramTypes, /* isInstanceFunction= */ true); } + /** + * Helper method for declaring a global function overload (e.g. {@code f(x, ...)}) with an + * automatically generated overload ID. + * + *

The returned declaration has an empty {@link #overloadId()}, which is automatically + * populated with the canonical signature (e.g. {@code "func(arg1,arg2)"}) when built into a + * {@link CelFunctionDecl} via {@link CelFunctionDecl.Builder#build()} or {@link + * CelFunctionDecl#newFunctionDeclaration}. + * + * @param resultType the return type of the overload + * @param paramTypes the argument types of the overload + */ + @CheckReturnValue + public static CelOverloadDecl newGlobalOverload(CelType resultType, CelType... paramTypes) { + return newGlobalOverload("", resultType, Arrays.asList(paramTypes)); + } + + /** + * Helper method for declaring a global function overload (e.g. {@code f(x, ...)}) with an + * automatically generated overload ID. + * + *

The returned declaration has an empty {@link #overloadId()}, which is automatically + * populated with the canonical signature (e.g. {@code "func(arg1,arg2)"}) when built into a + * {@link CelFunctionDecl} via {@link CelFunctionDecl.Builder#build()} or {@link + * CelFunctionDecl#newFunctionDeclaration}. + * + * @param resultType the return type of the overload + * @param paramTypes the argument types of the overload + */ + @CheckReturnValue + public static CelOverloadDecl newGlobalOverload(CelType resultType, List paramTypes) { + return newGlobalOverload("", /* doc= */ "", resultType, paramTypes); + } + /** Helper method for declaring a global function overload */ @CheckReturnValue public static CelOverloadDecl newGlobalOverload( diff --git a/common/src/main/java/dev/cel/common/CelSignatureUtils.java b/common/src/main/java/dev/cel/common/CelSignatureUtils.java new file mode 100644 index 000000000..05ae6095e --- /dev/null +++ b/common/src/main/java/dev/cel/common/CelSignatureUtils.java @@ -0,0 +1,249 @@ +// Copyright 2026 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; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import dev.cel.common.annotations.Internal; +import dev.cel.common.types.CelType; +import dev.cel.common.types.EnumType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.NullableType; +import dev.cel.common.types.TypeType; +import java.util.List; + +/** + * Internal utility methods for generating canonical CEL type and function overload signatures. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class CelSignatureUtils { + + /** + * Generates a canonical signature string for a {@link CelType}. + * + *

For example: + * + *

+ * + * @throws IllegalArgumentException if the type cannot be represented as a signature (e.g. {@code + * ERROR}). + */ + public static String makeTypeSignature(CelType type) { + checkNotNull(type); + StringBuilder result = new StringBuilder(); + appendTypeSignature(result, type); + return result.toString(); + } + + /** + * Generates a canonical overload ID / signature string for a function overload. + * + *

For example: + * + *

+ * + * @throws IllegalArgumentException if {@code isMember} is true and {@code args} is empty, or if + * any argument type is unsupported in signatures. + */ + public static String makeOverloadSignature( + String functionName, List args, boolean isMember) { + checkNotNull(functionName); + checkNotNull(args); + StringBuilder result = new StringBuilder(); + if (isMember) { + checkArgument(!args.isEmpty(), "Member function with no receiver"); + appendTypeSignature(result, args.get(0)); + result.append('.'); + } + appendEscaped(result, functionName, /* escapeDot= */ true); + result.append('('); + int startIndex = isMember ? 1 : 0; + for (int i = startIndex; i < args.size(); i++) { + if (i > startIndex) { + result.append(','); + } + appendTypeSignature(result, args.get(i)); + } + result.append(')'); + return result.toString(); + } + + private static void appendEscaped(StringBuilder result, String str, boolean escapeDot) { + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == '\\' + || c == '(' + || c == ')' + || c == '<' + || c == '>' + || c == '"' + || c == ',' + || c == '~' + || (escapeDot && c == '.')) { + result.append('\\'); + } + result.append(c); + } + } + + private static void appendTypeSignature(StringBuilder result, CelType type) { + checkNotNull(type); + if (type instanceof NullableType) { + CelType targetType = ((NullableType) type).targetType(); + switch (targetType.kind()) { + case BOOL: + result.append("bool_wrapper"); + return; + case INT: + result.append("int_wrapper"); + return; + case UINT: + result.append("uint_wrapper"); + return; + case DOUBLE: + result.append("double_wrapper"); + return; + case STRING: + result.append("string_wrapper"); + return; + case BYTES: + result.append("bytes_wrapper"); + return; + default: + throw new IllegalArgumentException("Unsupported wrapper type: " + type.name()); + } + } + if (type instanceof EnumType) { + appendEscaped(result, type.name(), /* escapeDot= */ false); + return; + } + + switch (type.kind()) { + case NULL_TYPE: + result.append("null"); + break; + case DYN: + result.append("dyn"); + break; + case BOOL: + result.append("bool"); + break; + case INT: + result.append("int"); + break; + case UINT: + result.append("uint"); + break; + case DOUBLE: + result.append("double"); + break; + case STRING: + result.append("string"); + break; + case BYTES: + result.append("bytes"); + break; + case ANY: + result.append("any"); + break; + case TIMESTAMP: + result.append("timestamp"); + break; + case DURATION: + result.append("duration"); + break; + case LIST: + ListType listType = (ListType) type; + result.append("list<"); + if (listType.hasElemType()) { + appendTypeSignature(result, listType.elemType()); + } else { + result.append("dyn"); + } + result.append('>'); + break; + case MAP: + MapType mapType = (MapType) type; + result.append("map<"); + appendTypeSignature(result, mapType.keyType()); + result.append(','); + appendTypeSignature(result, mapType.valueType()); + result.append('>'); + break; + case TYPE: + TypeType typeType = (TypeType) type; + result.append("type<"); + appendTypeSignature(result, typeType.type()); + result.append('>'); + break; + case TYPE_PARAM: + result.append('~'); + appendEscaped(result, type.name(), /* escapeDot= */ true); + break; + case OPAQUE: + if (type.name().equals("function")) { + result.append("function<"); + ImmutableList params = type.parameters(); + if (params.isEmpty()) { + result.append("dyn"); + } else { + for (int i = 0; i < params.size(); i++) { + if (i > 0) { + result.append(','); + } + appendTypeSignature(result, params.get(i)); + } + } + result.append('>'); + } else { + appendEscaped(result, type.name(), /* escapeDot= */ false); + ImmutableList params = type.parameters(); + if (!params.isEmpty()) { + result.append('<'); + for (int i = 0; i < params.size(); i++) { + if (i > 0) { + result.append(','); + } + appendTypeSignature(result, params.get(i)); + } + result.append('>'); + } + } + break; + case STRUCT: + appendEscaped(result, type.name(), /* escapeDot= */ false); + break; + case ERROR: + case UNSPECIFIED: + throw new IllegalArgumentException("Unsupported type in signature: " + type.name()); + } + } + + private CelSignatureUtils() {} +} diff --git a/common/src/test/java/dev/cel/common/BUILD.bazel b/common/src/test/java/dev/cel/common/BUILD.bazel index fb918db80..c9d707b65 100644 --- a/common/src/test/java/dev/cel/common/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/BUILD.bazel @@ -18,6 +18,7 @@ java_library( "//common:cel_descriptors", "//common:cel_issue", "//common:cel_proto_decl_converter", + "//common:cel_signature_utils", "//common:cel_source", "//common:cel_validation_exception", "//common:cel_var_decl", @@ -32,8 +33,11 @@ java_library( "//common/internal", "//common/internal:code_point_stream", "//common/types", + "//common/types:cel_internal_types", "//common/types:cel_proto_types", + "//common/types:cel_types", "//common/types:cel_v1alpha1_types", + "//common/types:type_providers", "//common/values:cel_byte_string", "//compiler", "//compiler:compiler_builder", diff --git a/common/src/test/java/dev/cel/common/CelSignatureUtilsTest.java b/common/src/test/java/dev/cel/common/CelSignatureUtilsTest.java new file mode 100644 index 000000000..52dfd0eaf --- /dev/null +++ b/common/src/test/java/dev/cel/common/CelSignatureUtilsTest.java @@ -0,0 +1,163 @@ +// Copyright 2026 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; + +import static com.google.common.truth.Truth.assertThat; +import static dev.cel.common.CelSignatureUtils.makeOverloadSignature; +import static dev.cel.common.CelSignatureUtils.makeTypeSignature; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypes; +import dev.cel.common.types.EnumType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.NullableType; +import dev.cel.common.types.OpaqueType; +import dev.cel.common.types.OptionalType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.types.TypeParamType; +import dev.cel.common.types.TypeType; +import dev.cel.common.types.UnspecifiedType; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class CelSignatureUtilsTest { + + private enum TypeSignatureTestCase { + NULL(SimpleType.NULL_TYPE, "null"), + DYN(SimpleType.DYN, "dyn"), + BOOL(SimpleType.BOOL, "bool"), + INT(SimpleType.INT, "int"), + UINT(SimpleType.UINT, "uint"), + DOUBLE(SimpleType.DOUBLE, "double"), + STRING(SimpleType.STRING, "string"), + BYTES(SimpleType.BYTES, "bytes"), + ANY(SimpleType.ANY, "any"), + TIMESTAMP(SimpleType.TIMESTAMP, "timestamp"), + DURATION(SimpleType.DURATION, "duration"), + BOOL_WRAPPER(NullableType.create(SimpleType.BOOL), "bool_wrapper"), + INT_WRAPPER(NullableType.create(SimpleType.INT), "int_wrapper"), + UINT_WRAPPER(NullableType.create(SimpleType.UINT), "uint_wrapper"), + DOUBLE_WRAPPER(NullableType.create(SimpleType.DOUBLE), "double_wrapper"), + STRING_WRAPPER(NullableType.create(SimpleType.STRING), "string_wrapper"), + BYTES_WRAPPER(NullableType.create(SimpleType.BYTES), "bytes_wrapper"), + LIST_INT(ListType.create(SimpleType.INT), "list"), + LIST_DYN(ListType.create(SimpleType.DYN), "list"), + MAP_STRING_INT(MapType.create(SimpleType.STRING, SimpleType.INT), "map"), + TYPE_INT(TypeType.create(SimpleType.INT), "type"), + TYPE_DYN(TypeType.create(SimpleType.DYN), "type"), + TYPE_PARAM_T(TypeParamType.create("T"), "~T"), + TYPE_PARAM_ESCAPED(TypeParamType.create("foo.bar"), "~foo\\.bar\\"), + OPTIONAL_STRING(OptionalType.create(SimpleType.STRING), "optional_type"), + OPAQUE_SIMPLE(OpaqueType.create("my.pkg.CustomType"), "my.pkg.CustomType"), + OPAQUE_PARAMETERIZED( + OpaqueType.create("my.pkg.CustomType", SimpleType.INT, TypeParamType.create("T")), + "my.pkg.CustomType"), + FUNCTION_NO_ARGS( + CelTypes.createFunctionType(SimpleType.INT, ImmutableList.of()), "function"), + FUNCTION_WITH_ARGS( + CelTypes.createFunctionType( + SimpleType.BOOL, ImmutableList.of(SimpleType.STRING, SimpleType.INT)), + "function"), + NESTED_COMPLEX( + MapType.create( + SimpleType.STRING, ListType.create(OptionalType.create(TypeParamType.create("V")))), + "map>>"); + + private final CelType type; + private final String expectedSignature; + + TypeSignatureTestCase(CelType type, String expectedSignature) { + this.type = type; + this.expectedSignature = expectedSignature; + } + } + + @Test + public void makeTypeSignature_success(@TestParameter TypeSignatureTestCase testCase) { + assertThat(makeTypeSignature(testCase.type)).isEqualTo(testCase.expectedSignature); + } + + @Test + public void makeTypeSignature_structAndEnum() { + CelType structType = StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes"); + CelType enumType = + EnumType.create( + "cel.expr.conformance.proto3.GlobalEnum", ImmutableMap.of("GOO", 0, "BAR", 1)); + + assertThat(makeTypeSignature(structType)).isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + assertThat(makeTypeSignature(enumType)).isEqualTo("cel.expr.conformance.proto3.GlobalEnum"); + } + + @Test + public void makeTypeSignature_unparameterizedListAndFunction() { + assertThat(makeTypeSignature(ListType.create())).isEqualTo("list"); + assertThat(makeTypeSignature(OpaqueType.create("function"))).isEqualTo("function"); + } + + @Test + public void makeTypeSignature_unsupportedTypes_throws() { + CelType unspecifiedType = UnspecifiedType.create(); + assertThrows(IllegalArgumentException.class, () -> makeTypeSignature(SimpleType.ERROR)); + assertThrows(IllegalArgumentException.class, () -> makeTypeSignature(unspecifiedType)); + } + + @Test + public void makeOverloadSignature_success() { + // Global no-arg function + assertThat(makeOverloadSignature("now", ImmutableList.of(), /* isMember= */ false)) + .isEqualTo("now()"); + + // Global multi-arg function + assertThat( + makeOverloadSignature( + "add", + ImmutableList.of(SimpleType.INT, ListType.create(SimpleType.STRING)), + /* isMember= */ false)) + .isEqualTo("add(int,list)"); + + // Member function with parameterized receiver + assertThat( + makeOverloadSignature( + "get", + ImmutableList.of( + MapType.create(TypeParamType.create("K"), TypeParamType.create("V")), + TypeParamType.create("K")), + /* isMember= */ true)) + .isEqualTo("map<~K,~V>.get(~K)"); + + // Global function with dot in name (escaped) + assertThat( + makeOverloadSignature( + "math.greatest", + ImmutableList.of(SimpleType.INT, SimpleType.INT), + /* isMember= */ false)) + .isEqualTo("math\\.greatest(int,int)"); + } + + @Test + public void makeOverloadSignature_memberWithNoArgs_throws() { + assertThrows( + IllegalArgumentException.class, + () -> makeOverloadSignature("size", ImmutableList.of(), /* isMember= */ true)); + } +}