diff --git a/checker/BUILD.bazel b/checker/BUILD.bazel index 306aa9a4d..a1e5ecf31 100644 --- a/checker/BUILD.bazel +++ b/checker/BUILD.bazel @@ -27,12 +27,6 @@ java_library( exports = ["//checker/src/main/java/dev/cel/checker:type_provider_legacy"], ) -java_library( - name = "type_provider_legacy_impl", - visibility = ["//:internal"], - exports = ["//checker/src/main/java/dev/cel/checker:type_provider_legacy_impl"], -) - java_library( name = "checker_legacy_environment", deprecation = "See go/cel-java-migration-guide. Please use CEL-Java Fluent APIs //compiler instead", diff --git a/checker/src/main/java/dev/cel/checker/BUILD.bazel b/checker/src/main/java/dev/cel/checker/BUILD.bazel index 1050de508..1eba32cde 100644 --- a/checker/src/main/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/main/java/dev/cel/checker/BUILD.bazel @@ -30,6 +30,7 @@ CHECKER_LEGACY_ENV_SOURCES = [ "ExprChecker.java", "ExprVisitor.java", "InferenceContext.java", + "LegacyTypeProviderBridge.java", "TypeFormatter.java", ] @@ -129,24 +130,6 @@ java_library( ], ) -java_library( - name = "type_provider_legacy_impl", - srcs = ["TypeProviderLegacyImpl.java"], - tags = [ - ], - deps = [ - ":type_provider_legacy", - "//common/annotations", - "//common/types", - "//common/types:cel_proto_types", - "//common/types:type_providers", - "@cel_spec//proto/cel/expr:checked_java_proto", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - "@maven//:org_jspecify_jspecify", - ], -) - java_library( name = "checker_legacy_environment", srcs = CHECKER_LEGACY_ENV_SOURCES, diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java index cf11013c7..d43f572e0 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java @@ -21,7 +21,7 @@ import dev.cel.expr.Decl; import dev.cel.expr.Type; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; +import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -48,10 +48,13 @@ import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.ProtoMessageType; import dev.cel.common.types.ProtoMessageTypeProvider; +import dev.cel.common.types.StructType; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.SortedSet; import java.util.TreeSet; import org.jspecify.annotations.Nullable; @@ -73,10 +76,28 @@ public final class CelCheckerLegacyImpl implements CelChecker, EnvVisitable { private final ImmutableSet functionDeclarations; private final Optional expectedResultType; + /** + * Preserved exclusively so {@link #toCheckerBuilder()} can round-trip caller-supplied legacy + * {@link TypeProvider} instances. Checker execution itself interacts strictly with {@link + * #celTypeProvider}. + */ @SuppressWarnings("Immutable") - private final @Nullable TypeProvider typeProvider; + private final @Nullable TypeProvider legacyTypeProvider; private final CelTypeProvider celTypeProvider; + + /** + * The type provider handed to {@link Env}. Identical to {@link #celTypeProvider}, except that a + * caller-supplied legacy {@link TypeProvider} is adapted onto it. + * + *

This is deliberately kept separate from {@link #celTypeProvider}: a {@link + * LegacyTypeProviderBridge} resolves types lazily and cannot enumerate them, so it must not + * escape through {@link #getTypeProvider()} into callers that iterate {@code types()} or {@code + * fieldNames()} (for example {@code ConstantFoldingOptimizer}), nor through {@link + * #toCheckerBuilder()}. + */ + private final CelTypeProvider envCelTypeProvider; + private final boolean standardEnvironmentEnabled; private final CelStandardDeclarations overriddenStandardDeclarations; @@ -124,12 +145,10 @@ public CelCheckerBuilder toCheckerBuilder() { .addFileTypes(fileDescriptors) .addProtoTypeMasks(protoTypeMasks); - if (typeProvider != null) { - builder.setTypeProvider(typeProvider); - } + expectedResultType.ifPresent(builder::setResultType); - if (expectedResultType.isPresent()) { - builder.setResultType(expectedResultType.get()); + if (legacyTypeProvider != null) { + builder.setTypeProvider(legacyTypeProvider); } if (overriddenStandardDeclarations != null) { @@ -166,13 +185,11 @@ public void accept(EnvVisitor envVisitor) { private Env getEnv(Errors errors) { Env env; if (overriddenStandardDeclarations != null) { - env = - Env.standard( - overriddenStandardDeclarations, errors, celTypeProvider, typeProvider, celOptions); + env = Env.standard(overriddenStandardDeclarations, errors, envCelTypeProvider, celOptions); } else if (standardEnvironmentEnabled) { - env = Env.standard(errors, celTypeProvider, typeProvider, celOptions); + env = Env.standard(errors, envCelTypeProvider, celOptions); } else { - env = Env.unconfigured(errors, celTypeProvider, typeProvider, celOptions); + env = Env.unconfigured(errors, envCelTypeProvider, celOptions); } identDeclarations.forEach(env::add); functionDeclarations.forEach(env::add); @@ -475,7 +492,7 @@ public CelCheckerLegacyImpl build() { container, identDeclarationSet, functionDeclarations.build(), - Optional.fromNullable(expectedResultType), + Optional.ofNullable(expectedResultType), customTypeProvider, messageTypeProvider, standardEnvironmentEnabled, @@ -497,13 +514,30 @@ private Builder() { } } + private static ImmutableList errorsToIssues(Errors errors) { + ImmutableList errorList = errors.getErrors(); + CelIssue.Builder issueBuilder = CelIssue.newBuilder().setSeverity(CelIssue.Severity.ERROR); + return errorList.stream() + .map( + e -> { + Errors.SourceLocation loc = errors.getPositionLocation(e.position()); + CelSourceLocation newLoc = CelSourceLocation.of(loc.line(), loc.column() - 1); + return issueBuilder + .setExprId(e.exprId()) + .setMessage(e.rawMessage()) + .setSourceLocation(newLoc) + .build(); + }) + .collect(toImmutableList()); + } + private CelCheckerLegacyImpl( CelOptions celOptions, CelContainer container, ImmutableSet identDeclarations, ImmutableSet functionDeclarations, Optional expectedResultType, - @Nullable TypeProvider typeProvider, + @Nullable TypeProvider legacyTypeProvider, CelTypeProvider celTypeProvider, boolean standardEnvironmentEnabled, @Nullable CelStandardDeclarations overriddenStandardDeclarations, @@ -516,8 +550,13 @@ private CelCheckerLegacyImpl( this.identDeclarations = identDeclarations; this.functionDeclarations = functionDeclarations; this.expectedResultType = expectedResultType; - this.typeProvider = typeProvider; + this.legacyTypeProvider = legacyTypeProvider; this.celTypeProvider = celTypeProvider; + this.envCelTypeProvider = + legacyTypeProvider == null + ? celTypeProvider + : new LegacyBridgeCombinedTypeProvider( + celTypeProvider, new LegacyTypeProviderBridge(legacyTypeProvider)); this.standardEnvironmentEnabled = standardEnvironmentEnabled; this.overriddenStandardDeclarations = overriddenStandardDeclarations; this.checkerLibraries = checkerLibraries; @@ -525,20 +564,84 @@ private CelCheckerLegacyImpl( this.protoTypeMasks = protoTypeMasks; } - private static ImmutableList errorsToIssues(Errors errors) { - ImmutableList errorList = errors.getErrors(); - CelIssue.Builder issueBuilder = CelIssue.newBuilder().setSeverity(CelIssue.Severity.ERROR); - return errorList.stream() - .map( - e -> { - Errors.SourceLocation loc = errors.getPositionLocation(e.position()); - CelSourceLocation newLoc = CelSourceLocation.of(loc.line(), loc.column() - 1); - return issueBuilder - .setExprId(e.exprId()) - .setMessage(e.rawMessage()) - .setSourceLocation(newLoc) - .build(); - }) - .collect(toImmutableList()); + @VisibleForTesting + @Immutable + static final class LegacyBridgeCombinedTypeProvider implements CelTypeProvider { + private final CelTypeProvider modernTypeProvider; + private final LegacyTypeProviderBridge legacyTypeProviderBridge; + private final CelTypeProvider.CombinedCelTypeProvider delegate; + + @Override + public ImmutableCollection types() { + return delegate.types(); + } + + @Override + public Optional findType(String typeName) { + return resolveType(typeName); + } + + private Optional resolveType(String typeName) { + Optional modernType = modernTypeProvider.findType(typeName); + if (modernType.isPresent()) { + CelType type = modernType.get(); + if (type instanceof ProtoMessageType) { + Optional legacyType = legacyTypeProviderBridge.findType(typeName); + if (legacyType.isPresent() && legacyType.get() instanceof ProtoMessageType) { + return Optional.of( + combineProtoMessageTypes( + (ProtoMessageType) type, (ProtoMessageType) legacyType.get())); + } + } + return modernType; + } + return legacyTypeProviderBridge.findType(typeName); + } + + private static ProtoMessageType combineProtoMessageTypes( + ProtoMessageType modern, ProtoMessageType legacy) { + boolean isEnumerable = true; + ImmutableSet fieldNames; + try { + fieldNames = modern.fieldNames(); + } catch (IllegalStateException e) { + isEnumerable = false; + fieldNames = ImmutableSet.of(); + } + + StructType.FieldResolver combinedExtensionResolver = + extensionName -> { + Optional modernExt = + modern.findExtension(extensionName).map(ProtoMessageType.Extension::type); + if (modernExt.isPresent()) { + return modernExt; + } + return legacy.findExtension(extensionName).map(ProtoMessageType.Extension::type); + }; + + if (!isEnumerable) { + return ProtoMessageType.createWithUnenumerableFields( + modern.name(), + fieldName -> modern.findField(fieldName).map(StructType.Field::type), + combinedExtensionResolver, + modern::isJsonName); + } + + return ProtoMessageType.create( + modern.name(), + fieldNames, + fieldName -> modern.findField(fieldName).map(StructType.Field::type), + combinedExtensionResolver, + modern::isJsonName); + } + + @VisibleForTesting + LegacyBridgeCombinedTypeProvider( + CelTypeProvider modernTypeProvider, LegacyTypeProviderBridge legacyTypeProviderBridge) { + this.modernTypeProvider = checkNotNull(modernTypeProvider); + this.legacyTypeProviderBridge = checkNotNull(legacyTypeProviderBridge); + this.delegate = + new CelTypeProvider.CombinedCelTypeProvider(modernTypeProvider, legacyTypeProviderBridge); + } } } diff --git a/checker/src/main/java/dev/cel/checker/Env.java b/checker/src/main/java/dev/cel/checker/Env.java index 97908dca3..af6fc0b34 100644 --- a/checker/src/main/java/dev/cel/checker/Env.java +++ b/checker/src/main/java/dev/cel/checker/Env.java @@ -83,14 +83,9 @@ public class Env { public static final CelFunctionDecl ERROR_FUNCTION_DECL = CelFunctionDecl.newBuilder().setName("*error*").build(); - private static final CelTypeProvider EMPTY_TYPE_PROVIDER = - new CelTypeProvider.CombinedCelTypeProvider(ImmutableList.of()); - /** Type provider responsible for resolving CEL types. */ private final CelTypeProvider celTypeProvider; - private final @Nullable TypeProvider legacyTypeProvider; - /** * Stack of declaration groups where each entry in stack represents a scope capable of hinding * declarations lower in the stack. @@ -133,12 +128,12 @@ static Env unconfigured(Errors errors, CelOptions celOptions) { return unconfigured(errors, new DescriptorTypeProvider(), celOptions); } - static Env unconfigured( - Errors errors, - CelTypeProvider celTypeProvider, - @Nullable TypeProvider legacyTypeProvider, - CelOptions celOptions) { - return new Env(errors, celTypeProvider, legacyTypeProvider, new DeclGroup(), celOptions); + /** + * Creates an unconfigured {@code Env} value without the standard CEL types, functions, and + * operators using a custom {@code celTypeProvider}. + */ + static Env unconfigured(Errors errors, CelTypeProvider celTypeProvider, CelOptions celOptions) { + return new Env(errors, celTypeProvider, new DeclGroup(), celOptions); } /** @@ -150,7 +145,7 @@ static Env unconfigured( */ @Deprecated public static Env unconfigured(Errors errors, TypeProvider typeProvider, CelOptions celOptions) { - return unconfigured(errors, EMPTY_TYPE_PROVIDER, typeProvider, celOptions); + return unconfigured(errors, new LegacyTypeProviderBridge(typeProvider), celOptions); } /** @@ -171,14 +166,8 @@ public static Env standard(Errors errors, TypeProvider typeProvider) { return standard(errors, typeProvider, LEGACY_TYPE_CHECKER_OPTIONS); } - static Env standard( - Errors errors, - CelTypeProvider celTypeProvider, - @Nullable TypeProvider legacyTypeProvider, - CelOptions celOptions) { - CelStandardDeclarations celStandardDeclaration = newStandardDeclarations(celOptions); - return standard( - celStandardDeclaration, errors, celTypeProvider, legacyTypeProvider, celOptions); + static Env standard(Errors errors, CelTypeProvider celTypeProvider, CelOptions celOptions) { + return standard(newStandardDeclarations(celOptions), errors, celTypeProvider, celOptions); } /** @@ -194,17 +183,15 @@ static Env standard( */ @Deprecated public static Env standard(Errors errors, TypeProvider typeProvider, CelOptions celOptions) { - return standard( - newStandardDeclarations(celOptions), errors, EMPTY_TYPE_PROVIDER, typeProvider, celOptions); + return standard(errors, new LegacyTypeProviderBridge(typeProvider), celOptions); } static Env standard( CelStandardDeclarations celStandardDeclaration, Errors errors, CelTypeProvider celTypeProvider, - @Nullable TypeProvider legacyTypeProvider, CelOptions celOptions) { - Env env = Env.unconfigured(errors, celTypeProvider, legacyTypeProvider, celOptions); + Env env = Env.unconfigured(errors, celTypeProvider, celOptions); // Isolate the standard declarations into their own scope for forward compatibility. celStandardDeclaration.functionDecls().forEach(env::add); celStandardDeclaration.identifierDecls().forEach(env::add); @@ -219,7 +206,8 @@ public static Env standard( Errors errors, TypeProvider typeProvider, CelOptions celOptions) { - return standard(celStandardDeclaration, errors, EMPTY_TYPE_PROVIDER, typeProvider, celOptions); + return standard( + celStandardDeclaration, errors, new LegacyTypeProviderBridge(typeProvider), celOptions); } private static CelStandardDeclarations newStandardDeclarations(CelOptions celOptions) { @@ -266,17 +254,6 @@ CelTypeProvider getCelTypeProvider() { return celTypeProvider; } - /** - * Returns the {@code TypeProvider}, or {@code null} if only modern {@link CelTypeProvider} was - * configured. - * - * @deprecated Use {@link #getCelTypeProvider()} instead. - */ - @Deprecated - @Nullable TypeProvider getTypeProvider() { - return legacyTypeProvider; - } - /** * Enters a new scope. All new declarations added to the environment exist only in this scope, and * will shadow declarations of the same name in outer scopes. This includes overloads in outer @@ -524,9 +501,6 @@ public Env add(String name, Type type) { .findType(cand) .filter(t -> !(t instanceof EnumType) || t.name().equals(cand)) .map(Env::wrapAsTypeIfNeeded); - if (!type.isPresent() && legacyTypeProvider != null) { - type = legacyTypeProvider.lookupCelType(cand); - } if (type.isPresent()) { decl = CelVarDecl.newVarDeclaration(cand, type.get()); decls.get(0).putIdent(decl); @@ -551,7 +525,7 @@ public Env add(String name, Type type) { } private Optional lookupEnumValue(String enumName) { - int dotIndex = enumName.lastIndexOf("."); + int dotIndex = enumName.lastIndexOf('.'); if (dotIndex > 0 && dotIndex < enumName.length() - 1) { String enumTypeName = enumName.substring(0, dotIndex); String localEnumName = enumName.substring(dotIndex + 1); @@ -563,23 +537,36 @@ private Optional lookupEnumValue(String enumName) { if (enumValue.isPresent()) { return enumValue; } - enumValue = - celTypeProvider - .findType(enumName) - .filter(t -> t instanceof EnumType) - .flatMap(t -> ((EnumType) t).findNumberByName(localEnumName)); - if (enumValue.isPresent()) { - return enumValue; - } + return celTypeProvider + .findType(enumName) + .filter(t -> t instanceof EnumType) + .flatMap(t -> ((EnumType) t).findNumberByName(localEnumName)); } - return Optional.ofNullable(legacyTypeProvider).map(value -> value.lookupEnumValue(enumName)); + return Optional.empty(); } private static CelType wrapAsTypeIfNeeded(CelType type) { if (type instanceof TypeType) { return type; } - return TypeType.create(type); + // CelTypeProvider resolves named types (STRUCT, OPAQUE, EnumType). These are wrapped into + // TypeType so that type identifiers imported into the expression scope have type 'type(T)'. + // OPAQUE types are wrapped here to support modern opaque type identifiers (e.g. + // 'optional_type') + // registered via DefaultTypeProvider or CelTypeProvider. + // + // Note: Legacy TypeProvider implementations (e.g., HierarchicalAttributeTypeProvider) abused + // lookupType to declare dynamic variables on the fly, returning variable types like ListType + // or SimpleType rather than type definitions. These must not be wrapped into TypeType so that + // they remain accessible as variable references rather than type literals. Standard type + // identifiers ('int', 'list', etc.) are pre-declared in CelStandardDeclarations and never hit + // this path. + if (type.kind().equals(CelKind.STRUCT) + || type.kind().equals(CelKind.OPAQUE) + || type instanceof EnumType) { + return TypeType.create(type); + } + return type; } /** @@ -1081,15 +1068,10 @@ static CelType getWellKnownType(CelType type) { } private Env( - Errors errors, - CelTypeProvider celTypeProvider, - @Nullable TypeProvider legacyTypeProvider, - DeclGroup declGroup, - CelOptions celOptions) { + Errors errors, CelTypeProvider celTypeProvider, DeclGroup declGroup, CelOptions celOptions) { this.celOptions = Preconditions.checkNotNull(celOptions); this.errors = Preconditions.checkNotNull(errors); this.celTypeProvider = Preconditions.checkNotNull(celTypeProvider); - this.legacyTypeProvider = legacyTypeProvider; this.decls.add(Preconditions.checkNotNull(declGroup)); } } diff --git a/checker/src/main/java/dev/cel/checker/ExprChecker.java b/checker/src/main/java/dev/cel/checker/ExprChecker.java index 8a842ce7a..85c2a8434 100644 --- a/checker/src/main/java/dev/cel/checker/ExprChecker.java +++ b/checker/src/main/java/dev/cel/checker/ExprChecker.java @@ -21,11 +21,11 @@ import dev.cel.expr.Type; import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; -import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.InlineMe; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; import dev.cel.common.CelFunctionDecl; @@ -65,6 +65,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.jspecify.annotations.Nullable; @@ -93,7 +94,7 @@ public final class ExprChecker { @CheckReturnValue @Deprecated public static CheckedExpr check(Env env, String inContainer, ParsedExpr parsedExpr) { - return typecheck(env, inContainer, parsedExpr, Optional.absent()); + return typecheck(env, inContainer, parsedExpr, com.google.common.base.Optional.absent()); } /** @@ -105,11 +106,11 @@ public static CheckedExpr check(Env env, String inContainer, ParsedExpr parsedEx @CheckReturnValue @Deprecated public static CheckedExpr typecheck( - Env env, String inContainer, ParsedExpr parsedExpr, Optional expectedResultType) { - Optional type = - expectedResultType.isPresent() - ? Optional.of(CelProtoTypes.typeToCelType(expectedResultType.get())) - : Optional.absent(); + Env env, + String inContainer, + ParsedExpr parsedExpr, + com.google.common.base.Optional expectedResultType) { + Optional type = expectedResultType.toJavaUtil().map(CelProtoTypes::typeToCelType); CelAbstractSyntaxTree ast = typecheck( env, @@ -171,11 +172,25 @@ public static CelAbstractSyntaxTree typecheck( typeMap); } + /** + * @deprecated Use {@link #typecheck(Env, CelContainer, CelAbstractSyntaxTree, Optional)} instead. + */ + @CheckReturnValue + @Internal + @Deprecated + @InlineMe( + replacement = "ExprChecker.typecheck(env, container, ast, expectedResultType.toJavaUtil())", + imports = "dev.cel.checker.ExprChecker") + public static CelAbstractSyntaxTree typecheck( + Env env, + CelContainer container, + CelAbstractSyntaxTree ast, + com.google.common.base.Optional expectedResultType) { + return typecheck(env, container, ast, expectedResultType.toJavaUtil()); + } + private final Env env; private final CelTypeProvider celTypeProvider; - - private final @Nullable TypeProvider legacyTypeProvider; - private final CelContainer container; private final Map positionMap; private final InferenceContext inferenceContext; @@ -791,23 +806,7 @@ private CelType getFieldType(long exprId, int position, CelType type, String fie return normalizeFieldType(extension.type()); } } - if (legacyTypeProvider != null) { - Optional extensionType = - lookupLegacyExtensionType(legacyTypeProvider, typeName, fieldName); - if (extensionType.isPresent()) { - return extensionType.get(); - } - } - env.reportError(exprId, position, "undefined field '%s'", fieldName); - return SimpleType.ERROR; - } - if (legacyTypeProvider != null && legacyTypeProvider.lookupCelType(typeName).isPresent()) { - Optional legacyFieldType = - lookupLegacyFieldType(legacyTypeProvider, type, fieldName); - if (legacyFieldType.isPresent()) { - return legacyFieldType.get(); - } env.reportError(exprId, position, "undefined field '%s'", fieldName); return SimpleType.ERROR; } @@ -855,27 +854,6 @@ private static CelType normalizeFieldType(CelType celType) { return celType; } - /** TODO: Remove after cl/984117942 is submitted. */ - private static Optional lookupLegacyFieldType( - TypeProvider legacyTypeProvider, CelType type, String fieldName) { - TypeProvider.FieldType legacyFieldType = legacyTypeProvider.lookupFieldType(type, fieldName); - if (legacyFieldType != null) { - return Optional.of(legacyFieldType.celType()); - } - return lookupLegacyExtensionType(legacyTypeProvider, type.name(), fieldName); - } - - private static Optional lookupLegacyExtensionType( - TypeProvider legacyTypeProvider, String typeName, String fieldName) { - TypeProvider.ExtensionFieldType extensionFieldType = - legacyTypeProvider.lookupExtensionType(fieldName); - if (extensionFieldType != null - && extensionFieldType.messageType().getMessageType().equals(typeName)) { - return Optional.of(extensionFieldType.fieldType().celType()); - } - return Optional.absent(); - } - /** Checks compatibility of joined types, and returns the most general common type. */ private CelType joinTypes(long exprId, int position, CelType previousType, CelType type) { if (previousType == null) { @@ -928,7 +906,6 @@ private ExprChecker( boolean namespacedDeclarations) { this.env = checkNotNull(env); this.celTypeProvider = env.getCelTypeProvider(); - this.legacyTypeProvider = env.getTypeProvider(); this.positionMap = checkNotNull(positionMap); this.container = checkNotNull(container); this.inferenceContext = checkNotNull(inferenceContext); diff --git a/checker/src/main/java/dev/cel/checker/LegacyTypeProviderBridge.java b/checker/src/main/java/dev/cel/checker/LegacyTypeProviderBridge.java new file mode 100644 index 000000000..ea41c2304 --- /dev/null +++ b/checker/src/main/java/dev/cel/checker/LegacyTypeProviderBridge.java @@ -0,0 +1,154 @@ +// 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.checker; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.types.CelKind; +import dev.cel.common.types.CelType; +import dev.cel.common.types.CelTypeProvider; +import dev.cel.common.types.EnumType; +import dev.cel.common.types.ProtoMessageType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.common.types.TypeType; +import java.util.Optional; + +/** + * Adapts a deprecated {@link TypeProvider} onto the modern {@link CelTypeProvider} interface. + * + *

It exists so that the checker internals ({@link Env} and {@link ExprChecker}) can be expressed + * purely in terms of {@link CelTypeProvider} while callers continue to supply a {@link + * TypeProvider}. The adaptation is applied at the legacy entrypoints, so no downstream migration is + * required. + * + *

Mapping the two contracts

+ * + *

The two interfaces disagree on what a type lookup returns. {@link + * TypeProvider#lookupType(String)} returns the type of the identifier naming the type (e.g. + * {@code type(foo.Bar)} for a message), whereas {@link CelTypeProvider#findType(String)} returns + * the type itself (e.g. the struct {@code foo.Bar}), which {@link Env} subsequently wraps. + * This bridge therefore unwraps {@code type(T)} before handing the result back. + * + *

Legacy providers which return an unwrapped struct from {@code lookupType} would previously + * declare the name as a variable of that message type. Such a provider is instead treated + * here as declaring a type name, matching {@code DescriptorTypeProvider}. No provider in google3 is + * known to do this. + * + *

CEL Library Internals. Do Not Use. + */ +@Immutable +final class LegacyTypeProviderBridge implements CelTypeProvider { + + // TypeProvider is a deprecated public interface that predates @Immutable annotations. Its + // implementations are required to be effectively immutable by the type-checker contract. + @SuppressWarnings("Immutable") + private final TypeProvider legacyTypeProvider; + + /** + * Returns an empty list: a {@link TypeProvider} resolves types lazily by name and cannot be + * enumerated, so callers must rely on {@link #findType} instead. + * + *

Consequently this provider must not be used with utilities that enumerate {@code types()} + * (for example {@code ProtoTypeMaskTypeProvider}), as they would silently observe no types at + * all. {@code CelCheckerLegacyImpl} keeps it off those paths by confining it to the provider it + * hands to {@link Env}. + */ + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return resolveType(typeName); + } + + private Optional resolveType(String typeName) { + Optional declaredType = legacyTypeProvider.lookupCelType(typeName); + if (declaredType.isPresent()) { + return declaredType.map(this::adaptDeclaredType); + } + return resolveEnumValueType(typeName); + } + + /** Converts an identifier's declared type, as returned by the legacy provider, into a type. */ + private CelType adaptDeclaredType(CelType declaredType) { + CelType targetType = + declaredType instanceof TypeType ? ((TypeType) declaredType).type() : declaredType; + if (targetType.kind().equals(CelKind.STRUCT)) { + return newStructType(targetType.name()); + } + // Not a struct, so Env will not re-wrap it. Hand back the declared type verbatim to preserve + // the legacy provider's intent, whether that is a type value (e.g. type(int)) or the declared + // type of a dynamically resolved qualified identifier (e.g. list(foo.Bar)). + return declaredType; + } + + private ProtoMessageType newStructType(String typeName) { + // Hoisted out of the resolver: the type-checker re-resolves the declaring type on every field + // selection, and the legacy lookup is keyed by a proto type rather than by name. + StructTypeReference typeReference = StructTypeReference.create(typeName); + return ProtoMessageType.createWithUnenumerableFields( + typeName, + fieldName -> findFieldType(typeReference, fieldName), + extensionName -> findExtensionType(typeName, extensionName)); + } + + private Optional findFieldType(StructTypeReference typeReference, String fieldName) { + TypeProvider.FieldType fieldType = legacyTypeProvider.lookupFieldType(typeReference, fieldName); + return Optional.ofNullable(fieldType).map(TypeProvider.FieldType::celType); + } + + private Optional findExtensionType(String typeName, String extensionName) { + TypeProvider.ExtensionFieldType extensionFieldType = + legacyTypeProvider.lookupExtensionType(extensionName); + if (extensionFieldType == null + || !extensionFieldType.messageType().getMessageType().equals(typeName)) { + return Optional.empty(); + } + return Optional.of(extensionFieldType.fieldType().celType()); + } + + /** + * Resolves a fully qualified enum value name (e.g. {@code foo.Bar.MyEnum.VALUE}) into an {@link + * EnumType} holding just that value. + * + *

A {@link TypeProvider} can only resolve enums one value at a time via {@link + * TypeProvider#lookupEnumValue}; it cannot enumerate an enum's values given only the enum's type + * name. The resulting single-valued {@code EnumType} is named after the enum type rather than the + * value, which is what allows {@code Env} to distinguish it from a type reference and resolve it + * as an enum constant instead. + */ + private Optional resolveEnumValueType(String enumValueName) { + int dotIndex = enumValueName.lastIndexOf('.'); + if (dotIndex <= 0 || dotIndex == enumValueName.length() - 1) { + return Optional.empty(); + } + Integer enumValue = legacyTypeProvider.lookupEnumValue(enumValueName); + if (enumValue == null) { + return Optional.empty(); + } + String enumTypeName = enumValueName.substring(0, dotIndex); + String localName = enumValueName.substring(dotIndex + 1); + return Optional.of(EnumType.create(enumTypeName, ImmutableMap.of(localName, enumValue))); + } + + LegacyTypeProviderBridge(TypeProvider legacyTypeProvider) { + this.legacyTypeProvider = checkNotNull(legacyTypeProvider); + } +} diff --git a/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java b/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java deleted file mode 100644 index b2ac51d95..000000000 --- a/checker/src/main/java/dev/cel/checker/TypeProviderLegacyImpl.java +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2022 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.checker; - -import dev.cel.expr.Type; -import com.google.common.collect.ImmutableSet; -import com.google.errorprone.annotations.CheckReturnValue; -import dev.cel.common.annotations.Internal; -import dev.cel.common.types.CelProtoTypes; -import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypeProvider; -import dev.cel.common.types.EnumType; -import dev.cel.common.types.ProtoMessageType; -import dev.cel.common.types.StructType; -import dev.cel.common.types.TypeType; -import java.util.Optional; -import org.jspecify.annotations.Nullable; - -/** - * The {@code TypeProviderLegacyImpl} acts as a bridge between the old and new type provider APIs - * - *

CEL Library Internals. Do Not Use. - */ -@CheckReturnValue -@Internal -final class TypeProviderLegacyImpl implements TypeProvider { - - private final CelTypeProvider celTypeProvider; - - TypeProviderLegacyImpl(CelTypeProvider celTypeProvider) { - this.celTypeProvider = celTypeProvider; - } - - @Override - public @Nullable Type lookupType(String typeName) { - return lookupCelType(typeName).map(CelProtoTypes::celTypeToType).orElse(null); - } - - @Override - public Optional lookupCelType(String typeName) { - return celTypeProvider.findType(typeName).map(TypeType::create); - } - - @Override - public @Nullable FieldType lookupFieldType(CelType type, String fieldName) { - String messageType = type.name(); - StructType structType = - (StructType) - celTypeProvider.findType(messageType).filter(t -> t instanceof StructType).orElse(null); - if (structType == null) { - return null; - } - - return structType - .findField(fieldName) - .map(f -> FieldType.of(CelProtoTypes.celTypeToType(f.type()))) - .orElse(null); - } - - @Override - public @Nullable FieldType lookupFieldType(Type type, String fieldName) { - return lookupFieldType(CelProtoTypes.typeToCelType(type), fieldName); - } - - @Override - public @Nullable ImmutableSet lookupFieldNames(Type type) { - String messageType = type.getMessageType(); - return celTypeProvider - .findType(messageType) - .filter(t -> t instanceof StructType) - .map(t -> ((StructType) t).fieldNames()) - .orElse(null); - } - - @Override - public @Nullable Integer lookupEnumValue(String enumName) { - int dotIndex = enumName.lastIndexOf("."); - if (dotIndex < 0 || dotIndex == enumName.length() - 1) { - return null; - } - String enumTypeName = enumName.substring(0, dotIndex); - String localEnumName = enumName.substring(dotIndex + 1); - return celTypeProvider - .findType(enumTypeName) - .filter(t -> t instanceof EnumType) - .flatMap(t -> ((EnumType) t).findNumberByName(localEnumName)) - .orElse(null); - } - - @Override - public @Nullable ExtensionFieldType lookupExtensionType(String extensionName) { - Optional extension = - celTypeProvider.types().stream() - .filter(t -> t instanceof ProtoMessageType) - .map(t -> (ProtoMessageType) t) - .map(t -> t.findExtension(extensionName)) - .filter(Optional::isPresent) - .map(Optional::get) - .findFirst(); - - return extension - .map( - et -> - ExtensionFieldType.of( - CelProtoTypes.celTypeToType(et.type()), - CelProtoTypes.celTypeToType(et.messageType()))) - .orElse(null); - } -} diff --git a/checker/src/test/java/dev/cel/checker/BUILD.bazel b/checker/src/test/java/dev/cel/checker/BUILD.bazel index 3028d6a2a..9957ab7fb 100644 --- a/checker/src/test/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/test/java/dev/cel/checker/BUILD.bazel @@ -20,7 +20,6 @@ java_library( "//checker:proto_type_mask", "//checker:standard_decl", "//checker:type_inferencer", - "//checker:type_provider_legacy_impl", "//common:cel_ast", "//common:cel_function_decl", "//common:cel_issue", diff --git a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java index 3e557d0eb..867d69bbd 100644 --- a/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java +++ b/checker/src/test/java/dev/cel/checker/CelCheckerLegacyImplTest.java @@ -17,12 +17,15 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import dev.cel.expr.Type; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.protobuf.Duration; import com.google.protobuf.FieldMask; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.checker.CelCheckerLegacyImpl.LegacyBridgeCombinedTypeProvider; import dev.cel.checker.CelStandardDeclarations.StandardFunction; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelContainer; @@ -33,12 +36,15 @@ import dev.cel.common.CelValidationException; import dev.cel.common.CelVarDecl; import dev.cel.common.ast.CelExpr; +import dev.cel.common.types.CelKind; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; import dev.cel.common.types.EnumType; import dev.cel.common.types.ListType; import dev.cel.common.types.MapType; +import dev.cel.common.types.ProtoMessageType; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructType; import dev.cel.common.types.StructTypeReference; import dev.cel.common.types.TypeType; import dev.cel.compiler.CelCompiler; @@ -323,6 +329,75 @@ public void check_undeclaredMessageTypeFieldSelection_throws() { + " the environment"); } + @Test + public void check_fieldSelection_withLegacyTypeProvider_success() throws Exception { + TypeProvider legacyTypeProvider = + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor())); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setTypeProvider(legacyTypeProvider) + .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("msg.single_int64").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.INT); + } + + @Test + public void check_undefinedFieldSelection_withLegacyTypeProvider_throws() { + TypeProvider legacyTypeProvider = + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor())); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setTypeProvider(legacyTypeProvider) + .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) + .build(); + + CelValidationException e = + assertThrows( + CelValidationException.class, () -> celCompiler.compile("msg.undefined").getAst()); + + assertThat(e).hasMessageThat().contains("undefined field 'undefined'"); + } + + @Test + public void check_messageCreation_withLegacyTypeProvider_success() throws Exception { + TypeProvider legacyTypeProvider = + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor())); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder().setTypeProvider(legacyTypeProvider).build(); + + CelType resultType = + celCompiler + .compile("cel.expr.conformance.proto3.TestAllTypes{single_int64: 2}") + .getAst() + .getResultType(); + + // Struct creation resolves to the type handed back by the type provider, which inherits + // identity equality, so the assertion is on kind and name rather than on an equal instance. + assertThat(resultType.kind()).isEqualTo(CelKind.STRUCT); + assertThat(resultType.name()).isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + } + + @Test + public void lookupEnumValue_legacyTypeProvider_success( + @TestParameter({ + "cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAZ == 2", + ".cel.expr.conformance.proto3.TestAllTypes.NestedEnum.BAZ == 2" + }) + String expr) + throws Exception { + TypeProvider legacyTypeProvider = + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor())); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder().setTypeProvider(legacyTypeProvider).build(); + + CelAbstractSyntaxTree ast = celCompiler.compile(expr).getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + @Test public void lookupEnumValue_modernTypeProvider_success( @TestParameter({ @@ -394,6 +469,359 @@ public Optional findType(String typeName) { assertThat(ast.getResultType()).isEqualTo(preWrappedType); } + @Test + public void check_legacyTypeProviderDeclaringNonStructType_success() throws Exception { + TypeProvider customLegacyProvider = + new TypeProvider() { + @Override + public Type lookupType(String typeName) { + if (typeName.equals("user.org_units")) { + return Type.newBuilder() + .setListType( + Type.ListType.newBuilder() + .setElemType(Type.newBuilder().setPrimitive(Type.PrimitiveType.INT64))) + .build(); + } + return null; + } + + @Override + public Integer lookupEnumValue(String enumName) { + return null; + } + + @Override + public FieldType lookupFieldType(Type type, String fieldName) { + return null; + } + }; + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .setTypeProvider(customLegacyProvider) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("size(user.org_units) == 0").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + private static CelCompiler newCompilerWithUnenumerableJsonNameMessage() { + ProtoMessageType modernUnenumerable = + ProtoMessageType.createWithUnenumerableFields( + "custom.UnenumerableMessage", + fieldName -> + fieldName.equals("myField") || fieldName.equals("my_field") + ? Optional.of(SimpleType.INT) + : Optional.empty(), + extensionName -> Optional.empty(), + "myField"::equals); + CelTypeProvider modernProvider = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return typeName.equals("custom.UnenumerableMessage") + ? Optional.of(modernUnenumerable) + : Optional.empty(); + } + }; + TypeProvider legacyTypeProvider = + new TypeProvider() { + @Override + public Type lookupType(String typeName) { + return null; + } + + @Override + public Integer lookupEnumValue(String enumName) { + return null; + } + + @Override + public FieldType lookupFieldType(Type type, String fieldName) { + return null; + } + }; + return CelCompilerFactory.standardCelCompilerBuilder() + .setTypeProvider(modernProvider) + .setTypeProvider(legacyTypeProvider) + .addVar("msg", StructTypeReference.create("custom.UnenumerableMessage")) + .build(); + } + + @Test + public void check_unenumerableMessageType_withJsonName_addsJsonNameExtension() throws Exception { + CelCompiler celCompiler = newCompilerWithUnenumerableJsonNameMessage(); + + CelAbstractSyntaxTree ast = celCompiler.compile("msg.myField == 1").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + assertThat(ast.getSource().getExtensions()) + .contains( + CelSource.Extension.create( + "json_name", + CelSource.Extension.Version.of(1, 1), + CelSource.Extension.Component.COMPONENT_RUNTIME)); + } + + @Test + public void check_unenumerableMessageType_withoutJsonName_doesNotAddExtension() throws Exception { + CelCompiler celCompiler = newCompilerWithUnenumerableJsonNameMessage(); + + CelAbstractSyntaxTree nonJsonAst = celCompiler.compile("msg.my_field == 1").getAst(); + + assertThat(nonJsonAst.getResultType()).isEqualTo(SimpleType.BOOL); + assertThat(nonJsonAst.getSource().getExtensions()).isEmpty(); + } + + @Test + public void check_extensionField_withUnenumerableModernMessageAndLegacyExtensionProvider_success() + throws Exception { + ProtoMessageType unenumerableMessage = + ProtoMessageType.createWithUnenumerableFields( + "cel.expr.conformance.proto2.TestAllTypes", + fieldName -> Optional.empty(), + extensionName -> Optional.empty(), + unused -> false); + CelTypeProvider modernProvider = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return typeName.equals("cel.expr.conformance.proto2.TestAllTypes") + ? Optional.of(unenumerableMessage) + : Optional.empty(); + } + }; + TypeProvider legacyExtensionProvider = + new DescriptorTypeProvider( + ImmutableList.of( + TestAllTypesProto.getDescriptor(), TestAllTypesExtensions.getDescriptor())); + CelChecker celChecker = + CelCompilerFactory.standardCelCheckerBuilder() + .setTypeProvider(modernProvider) + .setTypeProvider(legacyExtensionProvider) + .addVarDeclarations( + CelVarDecl.newVarDeclaration( + "msg", StructTypeReference.create("cel.expr.conformance.proto2.TestAllTypes"))) + .build(); + CelAbstractSyntaxTree parsedAst = + CelAbstractSyntaxTree.newParsedAst( + CelExpr.ofSelect( + 2L, + CelExpr.ofIdent(1L, "msg"), + "cel.expr.conformance.proto2.int32_ext", + /* isTestOnly= */ false), + CelSource.newBuilder().build()); + + CelAbstractSyntaxTree checkedAst = celChecker.check(parsedAst).getAst(); + + assertThat(checkedAst.getResultType()).isEqualTo(SimpleType.INT); + } + + @Test + public void combinedTypeProvider_enumerableModernMessage_preservesFieldNames() { + ProtoMessageType modernMessage = + ProtoMessageType.create( + "cel.expr.conformance.proto3.TestAllTypes", + ImmutableSet.of("single_int32", "single_int64"), + fieldName -> Optional.of(SimpleType.INT), + extensionName -> Optional.empty(), + /* jsonNameResolver= */ fieldName -> false); + CelTypeProvider modernProvider = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return typeName.equals("cel.expr.conformance.proto3.TestAllTypes") + ? Optional.of(modernMessage) + : Optional.empty(); + } + }; + LegacyTypeProviderBridge legacyBridge = + new LegacyTypeProviderBridge( + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor()))); + LegacyBridgeCombinedTypeProvider combinedProvider = + new LegacyBridgeCombinedTypeProvider(modernProvider, legacyBridge); + + Optional resolvedType = + combinedProvider.findType("cel.expr.conformance.proto3.TestAllTypes"); + + assertThat( + resolvedType + .filter(t -> t instanceof ProtoMessageType) + .map(t -> ((ProtoMessageType) t).fieldNames())) + .hasValue(ImmutableSet.of("single_int32", "single_int64")); + assertThat( + resolvedType + .filter(t -> t instanceof ProtoMessageType) + .flatMap(t -> ((ProtoMessageType) t).findField("single_int32")) + .map(StructType.Field::type)) + .hasValue(SimpleType.INT); + } + + @Test + public void combinedTypeProvider_unenumerableModernMessage_fieldNamesThrows() { + ProtoMessageType modernMessage = + ProtoMessageType.createWithUnenumerableFields( + "cel.expr.conformance.proto3.TestAllTypes", + fieldName -> Optional.empty(), + extensionName -> Optional.empty(), + fieldName -> false); + CelTypeProvider modernProvider = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return typeName.equals("cel.expr.conformance.proto3.TestAllTypes") + ? Optional.of(modernMessage) + : Optional.empty(); + } + }; + LegacyTypeProviderBridge legacyBridge = + new LegacyTypeProviderBridge( + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor()))); + LegacyBridgeCombinedTypeProvider combinedProvider = + new LegacyBridgeCombinedTypeProvider(modernProvider, legacyBridge); + + Optional resolvedType = + combinedProvider.findType("cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(resolvedType.map(CelType::kind)).hasValue(CelKind.STRUCT); + ProtoMessageType protoMessageType = (ProtoMessageType) resolvedType.get(); + IllegalStateException thrown = + assertThrows(IllegalStateException.class, protoMessageType::fieldNames); + assertThat(thrown).hasMessageThat().contains("cannot be enumerated"); + assertThrows(IllegalStateException.class, protoMessageType::fields); + } + + @Test + public void combinedTypeProvider_unenumerableModernMessage_delegatesJsonNameResolverToModern() { + ProtoMessageType modernMessage = + ProtoMessageType.createWithUnenumerableFields( + "cel.expr.conformance.proto3.TestAllTypes", + fieldName -> + fieldName.equals("modernField") ? Optional.of(SimpleType.STRING) : Optional.empty(), + extensionName -> Optional.empty(), + "modernJsonField"::equals); + CelTypeProvider modernProvider = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return typeName.equals("cel.expr.conformance.proto3.TestAllTypes") + ? Optional.of(modernMessage) + : Optional.empty(); + } + }; + LegacyTypeProviderBridge legacyBridge = + new LegacyTypeProviderBridge( + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor()))); + LegacyBridgeCombinedTypeProvider combinedProvider = + new LegacyBridgeCombinedTypeProvider(modernProvider, legacyBridge); + + Optional resolvedType = + combinedProvider.findType("cel.expr.conformance.proto3.TestAllTypes"); + + assertThat( + resolvedType + .filter(t -> t instanceof ProtoMessageType) + .flatMap(t -> ((ProtoMessageType) t).findField("modernField")) + .map(StructType.Field::type)) + .hasValue(SimpleType.STRING); + assertThat( + resolvedType + .filter(t -> t instanceof ProtoMessageType) + .map(t -> ((ProtoMessageType) t).isJsonName("modernJsonField"))) + .hasValue(true); + assertThat( + resolvedType + .filter(t -> t instanceof ProtoMessageType) + .map(t -> ((ProtoMessageType) t).isJsonName("nonExistent"))) + .hasValue(false); + } + + @Test + public void combinedTypeProvider_modernExtensionTakesPrecedenceOverLegacy() { + ProtoMessageType modernMessage = + ProtoMessageType.createWithUnenumerableFields( + "cel.expr.conformance.proto2.TestAllTypes", + fieldName -> Optional.empty(), + extensionName -> + extensionName.equals("cel.expr.conformance.proto2.int32_ext") + ? Optional.of(SimpleType.STRING) + : Optional.empty(), + fieldName -> false); + CelTypeProvider modernProvider = + new CelTypeProvider() { + @Override + public ImmutableList types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return typeName.equals("cel.expr.conformance.proto2.TestAllTypes") + ? Optional.of(modernMessage) + : Optional.empty(); + } + }; + LegacyTypeProviderBridge legacyBridge = + new LegacyTypeProviderBridge( + new DescriptorTypeProvider( + ImmutableList.of( + TestAllTypesProto.getDescriptor(), TestAllTypesExtensions.getDescriptor()))); + LegacyBridgeCombinedTypeProvider combinedProvider = + new LegacyBridgeCombinedTypeProvider(modernProvider, legacyBridge); + + Optional resolvedType = + combinedProvider.findType("cel.expr.conformance.proto2.TestAllTypes"); + + assertThat( + resolvedType + .filter(t -> t instanceof ProtoMessageType) + .map(t -> (ProtoMessageType) t) + .flatMap(t -> t.findExtension("cel.expr.conformance.proto2.int32_ext")) + .map(ProtoMessageType.Extension::type)) + .hasValue(SimpleType.STRING); + } + + @Test + public void check_regularField_withModernMessageAndLegacyTypeProvider_success() throws Exception { + TypeProvider legacyTypeProvider = + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor())); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addMessageTypes(TestAllTypes.getDescriptor()) + .setTypeProvider(legacyTypeProvider) + .addVar("msg", StructTypeReference.create("cel.expr.conformance.proto3.TestAllTypes")) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("msg.single_int32 == 1").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + private enum FieldTypeTestCase { REPEATED_PRIMITIVE("msg.repeated_int64", ListType.create(SimpleType.INT)), MAP_PRIMITIVE("msg.map_string_string", MapType.create(SimpleType.STRING, SimpleType.STRING)), diff --git a/checker/src/test/java/dev/cel/checker/LegacyTypeProviderBridgeTest.java b/checker/src/test/java/dev/cel/checker/LegacyTypeProviderBridgeTest.java new file mode 100644 index 000000000..647405654 --- /dev/null +++ b/checker/src/test/java/dev/cel/checker/LegacyTypeProviderBridgeTest.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.checker; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import dev.cel.expr.Type; +import com.google.common.collect.ImmutableList; +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.EnumType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.ProtoMessageType; +import dev.cel.common.types.SimpleType; +import dev.cel.common.types.StructType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; +import dev.cel.expr.conformance.proto2.TestAllTypesProto; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import java.util.Optional; +import org.jspecify.annotations.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class LegacyTypeProviderBridgeTest { + + private static final String TEST_ALL_TYPES = "cel.expr.conformance.proto3.TestAllTypes"; + private static final String PROTO2_TEST_ALL_TYPES = "cel.expr.conformance.proto2.TestAllTypes"; + private static final String INT32_EXT = "cel.expr.conformance.proto2.int32_ext"; + + private final LegacyTypeProviderBridge bridge = + new LegacyTypeProviderBridge( + new DescriptorTypeProvider(ImmutableList.of(TestAllTypes.getDescriptor()))); + + // DescriptorTypeProvider only walks the file descriptors it is given, so the file declaring the + // extended message must be registered alongside the file declaring the extensions. + private final LegacyTypeProviderBridge proto2Bridge = + new LegacyTypeProviderBridge( + new DescriptorTypeProvider( + ImmutableList.of( + TestAllTypesExtensions.getDescriptor(), TestAllTypesProto.getDescriptor()))); + + @Test + public void types_isEmpty() { + assertThat(bridge.types()).isEmpty(); + } + + @Test + public void findType_messageType_returnsStructTypeOfSameName() { + Optional type = bridge.findType(TEST_ALL_TYPES); + + assertThat(type.map(Object::getClass)).hasValue(ProtoMessageType.class); + assertThat(type.map(CelType::name)).hasValue(TEST_ALL_TYPES); + } + + @Test + public void findType_absentName_returnsEmpty() { + LazyTypeProvider lazyTypeProvider = new LazyTypeProvider(); + LegacyTypeProviderBridge lazyBridge = new LegacyTypeProviderBridge(lazyTypeProvider); + + assertThat(lazyBridge.findType("foo.Bar")).isEmpty(); + } + + @Test + public void findType_absentNameNotCached_resolvesWhenPopulated() { + // Legacy providers may be backed by lazily populated descriptor pools, so a miss must not be + // cached as a permanent negative. + LazyTypeProvider lazyTypeProvider = new LazyTypeProvider(); + LegacyTypeProviderBridge lazyBridge = new LegacyTypeProviderBridge(lazyTypeProvider); + lazyBridge.findType("foo.Bar"); + + lazyTypeProvider.declaredType = StructTypeReference.create("foo.Bar"); + + assertThat(lazyBridge.findType("foo.Bar").map(CelType::name)).hasValue("foo.Bar"); + } + + @Test + public void findType_unresolvableName_returnsEmpty( + @TestParameter({ + // Not declared by the provider. + "cel.expr.conformance.proto3.Undefined", + // A legacy provider can only resolve enums one value at a time, so the enum type on + // its own is not resolvable. + "cel.expr.conformance.proto3.TestAllTypes.NestedEnum", + "cel.expr.conformance.proto3.TestAllTypes.NestedEnum.UNDEFINED", + // Unqualified, so there is no enum type name to split off. + "TestAllTypes", + // Degenerate qualifications. + "cel.expr.conformance.proto3.TestAllTypes.NestedEnum.", + ".BAZ" + }) + String typeName) { + assertThat(bridge.findType(typeName)).isEmpty(); + } + + @Test + public void findField_declaredField_returnsFieldType() { + StructType structType = getStructType(TEST_ALL_TYPES); + + Optional field = structType.findField("single_int64"); + + assertThat(field.map(StructType.Field::type)).hasValue(SimpleType.INT); + } + + @Test + public void findField_messageField_returnsStructTypeReference() { + StructType structType = getStructType(TEST_ALL_TYPES); + + Optional field = structType.findField("single_nested_message"); + + assertThat(field.map(StructType.Field::type)) + .hasValue(StructTypeReference.create(TEST_ALL_TYPES + ".NestedMessage")); + } + + @Test + public void findField_undefinedField_returnsEmpty() { + StructType structType = getStructType(TEST_ALL_TYPES); + + assertThat(structType.findField("undefined_field")).isEmpty(); + } + + @Test + public void fieldNames_bridgedType_throws() { + StructType structType = getStructType(TEST_ALL_TYPES); + + // The bridge cannot enumerate field names, so enumeration must fail loudly rather than + // silently report that the message has no fields. + assertThrows(IllegalStateException.class, structType::fieldNames); + } + + @Test + public void fields_bridgedType_throws() { + StructType structType = getStructType(TEST_ALL_TYPES); + + assertThrows(IllegalStateException.class, structType::fields); + } + + @Test + public void findExtension_declaredOnType_returnsFieldType() { + ProtoMessageType structType = getProtoMessageType(PROTO2_TEST_ALL_TYPES); + + Optional extension = structType.findExtension(INT32_EXT); + + assertThat(extension.map(ProtoMessageType.Extension::type)).hasValue(SimpleType.INT); + } + + @Test + public void findExtension_declaredOnOtherType_returnsEmpty() { + ProtoMessageType structType = + getProtoMessageType("cel.expr.conformance.proto2.Proto2ExtensionScopedMessage"); + + assertThat(structType.findExtension(INT32_EXT)).isEmpty(); + } + + @Test + public void findType_enumValue_returnsEnumTypeNamedAfterEnum() { + Optional type = bridge.findType(TEST_ALL_TYPES + ".NestedEnum.BAZ"); + + assertThat(type.map(Object::getClass)).hasValue(EnumType.class); + // Naming the type rather than the value is what lets Env distinguish an enum constant from a + // type reference. + assertThat(type.map(CelType::name)).hasValue(TEST_ALL_TYPES + ".NestedEnum"); + assertThat( + type.filter(t -> t instanceof EnumType) + .flatMap(t -> ((EnumType) t).findNumberByName("BAZ"))) + .hasValue(2); + } + + @Test + public void findType_nonStructDeclaredType_returnedVerbatim() { + // Not every legacy provider declares message types. Anything that is not a struct is handed + // back unchanged so that Env observes the type the provider intended to declare. + LazyTypeProvider lazyTypeProvider = new LazyTypeProvider(); + lazyTypeProvider.declaredType = ListType.create(SimpleType.STRING); + + assertThat(new LegacyTypeProviderBridge(lazyTypeProvider).findType("some.declared.Name")) + .hasValue(lazyTypeProvider.declaredType); + } + + @Test + public void findType_enumValueWithTrailingDot_returnsEmpty() { + LazyTypeProvider lazyTypeProvider = new LazyTypeProvider(); + lazyTypeProvider.enumValue = 1; + + assertThat(new LegacyTypeProviderBridge(lazyTypeProvider).findType("foo.Bar.")).isEmpty(); + } + + @Test + public void findType_enumValueWithLeadingDot_returnsEmpty() { + LazyTypeProvider lazyTypeProvider = new LazyTypeProvider(); + lazyTypeProvider.enumValue = 1; + + assertThat(new LegacyTypeProviderBridge(lazyTypeProvider).findType(".Bar")).isEmpty(); + } + + private StructType getStructType(String typeName) { + Optional type = bridge.findType(typeName); + assertThat(type.map(CelType::name)).hasValue(typeName); + return (StructType) type.get(); + } + + private ProtoMessageType getProtoMessageType(String typeName) { + Optional type = proto2Bridge.findType(typeName); + assertThat(type.map(CelType::name)).hasValue(typeName); + return (ProtoMessageType) type.get(); + } + + /** A {@link TypeProvider} whose single declared type can be populated after construction. */ + private static final class LazyTypeProvider implements TypeProvider { + + private @Nullable CelType declaredType; + private @Nullable Integer enumValue; + + @Override + public @Nullable Type lookupType(String typeName) { + throw new UnsupportedOperationException("lookupType is not implemented"); + } + + @Override + public Optional lookupCelType(String typeName) { + return Optional.ofNullable(declaredType); + } + + @Override + public @Nullable Integer lookupEnumValue(String enumName) { + return enumValue; + } + + @Override + public @Nullable FieldType lookupFieldType(Type type, String fieldName) { + return null; + } + } +} diff --git a/checker/src/test/java/dev/cel/checker/TypeProviderLegacyImplTest.java b/checker/src/test/java/dev/cel/checker/TypeProviderLegacyImplTest.java deleted file mode 100644 index 4569877c3..000000000 --- a/checker/src/test/java/dev/cel/checker/TypeProviderLegacyImplTest.java +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2022 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.checker; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; - -import dev.cel.expr.Type; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.protobuf.Descriptors.Descriptor; -import dev.cel.common.types.CelProtoTypes; -import dev.cel.common.types.ProtoMessageTypeProvider; -import dev.cel.expr.conformance.proto2.Proto2ExtensionScopedMessage; -import dev.cel.expr.conformance.proto2.TestAllTypes; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public final class TypeProviderLegacyImplTest { - - private static final ImmutableList DESCRIPTORS = - ImmutableList.of(TestAllTypes.getDescriptor(), Proto2ExtensionScopedMessage.getDescriptor()); - - private final ProtoMessageTypeProvider proto2Provider = new ProtoMessageTypeProvider(DESCRIPTORS); - - private final DescriptorTypeProvider descriptorTypeProvider = - new DescriptorTypeProvider(DESCRIPTORS); - - private final TypeProviderLegacyImpl compatTypeProvider = - new TypeProviderLegacyImpl(proto2Provider); - - @Test - public void lookupType() { - assertThat(compatTypeProvider.lookupType("cel.expr.conformance.proto2.TestAllTypes")) - .isEqualTo(descriptorTypeProvider.lookupType("cel.expr.conformance.proto2.TestAllTypes")); - assertThat(compatTypeProvider.lookupType("not.registered.TypeName")) - .isEqualTo(descriptorTypeProvider.lookupType("not.registered.TypeName")); - } - - @Test - public void lookupFieldNames() { - Type nestedTestAllTypes = - compatTypeProvider.lookupType("cel.expr.conformance.proto2.NestedTestAllTypes").getType(); - ImmutableSet fieldNames = compatTypeProvider.lookupFieldNames(nestedTestAllTypes); - assertThat(fieldNames) - .containsExactlyElementsIn(descriptorTypeProvider.lookupFieldNames(nestedTestAllTypes)); - assertThat(fieldNames).containsExactly("payload", "child"); - } - - @Test - public void lookupFieldType() { - Type nestedTestAllTypes = - compatTypeProvider.lookupType("cel.expr.conformance.proto2.NestedTestAllTypes").getType(); - assertThat(compatTypeProvider.lookupFieldType(nestedTestAllTypes, "payload")) - .isEqualTo(descriptorTypeProvider.lookupFieldType(nestedTestAllTypes, "payload")); - assertThat(compatTypeProvider.lookupFieldType(nestedTestAllTypes, "child")) - .isEqualTo(descriptorTypeProvider.lookupFieldType(nestedTestAllTypes, "child")); - } - - @Test - public void lookupFieldType_inputNotMessage() { - Type globalEnumType = - compatTypeProvider.lookupType("cel.expr.conformance.proto2.GlobalEnum").getType(); - assertThat(compatTypeProvider.lookupFieldType(globalEnumType, "payload")).isNull(); - assertThat(compatTypeProvider.lookupFieldType(globalEnumType, "payload")) - .isEqualTo(descriptorTypeProvider.lookupFieldType(globalEnumType, "payload")); - } - - @Test - public void lookupExtension() { - TypeProvider.ExtensionFieldType extensionType = - compatTypeProvider.lookupExtensionType("cel.expr.conformance.proto2.nested_enum_ext"); - assertThat(extensionType.messageType()) - .isEqualTo(CelProtoTypes.createMessage("cel.expr.conformance.proto2.TestAllTypes")); - assertThat(extensionType.fieldType().type()).isEqualTo(CelProtoTypes.INT64); - assertThat(extensionType) - .isEqualTo( - descriptorTypeProvider.lookupExtensionType( - "cel.expr.conformance.proto2.nested_enum_ext")); - } - - @Test - public void lookupEnumValue() { - Integer enumValue = - compatTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.GlobalEnum.GAR"); - assertThat(enumValue).isEqualTo(1); - assertThat(enumValue) - .isEqualTo( - descriptorTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.GlobalEnum.GAR")); - } - - @Test - public void lookupEnumValue_notFoundValue() { - Integer enumValue = - compatTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.GlobalEnum.BAR"); - assertThat(enumValue).isNull(); - assertThat(enumValue) - .isEqualTo( - descriptorTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.GlobalEnum.BAR")); - } - - @Test - public void lookupEnumValue_notFoundEnumType() { - Integer enumValue = - compatTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.InvalidEnum.TEST"); - assertThat(enumValue).isNull(); - assertThat(enumValue) - .isEqualTo( - descriptorTypeProvider.lookupEnumValue("cel.expr.conformance.proto2.InvalidEnum.TEST")); - } - - @Test - public void lookupEnumValue_notFoundBadEnumName() { - assertThat(compatTypeProvider.lookupEnumValue("TEST")).isNull(); - assertThat(compatTypeProvider.lookupEnumValue("TEST.")).isNull(); - assertThat(descriptorTypeProvider.lookupEnumValue("TEST")).isNull(); - assertThat(descriptorTypeProvider.lookupEnumValue("TEST.")).isNull(); - } -} diff --git a/common/src/main/java/dev/cel/common/types/ProtoMessageType.java b/common/src/main/java/dev/cel/common/types/ProtoMessageType.java index 11e48bbe6..531c5a4a9 100644 --- a/common/src/main/java/dev/cel/common/types/ProtoMessageType.java +++ b/common/src/main/java/dev/cel/common/types/ProtoMessageType.java @@ -14,10 +14,14 @@ package dev.cel.common.types; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; import java.util.Optional; /** @@ -30,16 +34,29 @@ public final class ProtoMessageType extends StructType { private final StructType.FieldResolver extensionResolver; private final JsonNameResolver jsonNameResolver; + private final boolean fieldNamesEnumerable; - ProtoMessageType( - String name, - ImmutableSet fieldNames, - StructType.FieldResolver fieldResolver, - StructType.FieldResolver extensionResolver, - JsonNameResolver jsonNameResolver) { - super(name, fieldNames, fieldResolver); - this.extensionResolver = extensionResolver; - this.jsonNameResolver = jsonNameResolver; + @Override + public Optional findField(String fieldName) { + if (fieldNamesEnumerable) { + return super.findField(fieldName); + } + // The set of declared field names is unknown, so the resolver is the sole source of truth. + return fieldResolver.findField(fieldName).map(type -> Field.of(fieldName, type)); + } + + @Override + public ImmutableSet fieldNames() { + checkState( + fieldNamesEnumerable, "fields of '%s' cannot be enumerated; use findField instead", name); + return super.fieldNames(); + } + + @Override + public ImmutableSet fields() { + checkState( + fieldNamesEnumerable, "fields of '%s' cannot be enumerated; use findField instead", name); + return super.fields(); } /** Find an {@code Extension} by its fully-qualified {@code extensionName}. */ @@ -57,6 +74,9 @@ public boolean isJsonName(String fieldName) { /** * Create a new instance of the {@code ProtoMessageType} using the {@code visibleFields} set as a * mask of the fields from the backing proto. + * + *

The returned type is always enumerable, including when this type is not: {@code + * visibleFields} is by definition the complete set of field names the masked type exposes. */ public ProtoMessageType withVisibleFields(ImmutableSet visibleFields) { return new ProtoMessageType( @@ -73,6 +93,74 @@ public static ProtoMessageType create( name, fieldNames, fieldResolver, extensionResolver, jsonNameResolver); } + /** + * Creates a {@code ProtoMessageType} for a message whose set of field names cannot be enumerated + * ahead of time, such as one backed by a lazily populated descriptor pool or by a deprecated + * {@code dev.cel.checker.TypeProvider}. + * + *

{@link #findField} delegates every lookup directly to {@code fieldResolver} rather than + * first consulting {@link #fieldNames()}. Enumerating the resulting type via {@link #fieldNames} + * or {@link #fields} throws, so such a type must not be handed to utilities that iterate fields + * (for example {@code ProtoTypeMaskTypeProvider} or {@code ConstantFoldingOptimizer}). + * + *

CEL Library Internals. Do Not Use. + */ + @Internal + public static ProtoMessageType createWithUnenumerableFields( + String name, FieldResolver fieldResolver, FieldResolver extensionResolver) { + return createWithUnenumerableFields( + name, fieldResolver, extensionResolver, /* jsonNameResolver= */ fieldName -> false); + } + + /** + * Creates a {@code ProtoMessageType} for a message whose set of field names cannot be enumerated + * ahead of time, with a custom {@code jsonNameResolver}. + * + *

CEL Library Internals. Do Not Use. + */ + @Internal + public static ProtoMessageType createWithUnenumerableFields( + String name, + FieldResolver fieldResolver, + FieldResolver extensionResolver, + JsonNameResolver jsonNameResolver) { + return new ProtoMessageType( + checkNotNull(name), + ImmutableSet.of(), + checkNotNull(fieldResolver), + checkNotNull(extensionResolver), + checkNotNull(jsonNameResolver), + /* fieldNamesEnumerable= */ false); + } + + private ProtoMessageType( + String name, + ImmutableSet fieldNames, + FieldResolver fieldResolver, + FieldResolver extensionResolver, + JsonNameResolver jsonNameResolver) { + this( + name, + fieldNames, + fieldResolver, + extensionResolver, + jsonNameResolver, + /* fieldNamesEnumerable= */ true); + } + + private ProtoMessageType( + String name, + ImmutableSet fieldNames, + FieldResolver fieldResolver, + FieldResolver extensionResolver, + JsonNameResolver jsonNameResolver, + boolean fieldNamesEnumerable) { + super(name, fieldNames, fieldResolver); + this.extensionResolver = extensionResolver; + this.jsonNameResolver = jsonNameResolver; + this.fieldNamesEnumerable = fieldNamesEnumerable; + } + /** Functional interface for resolving whether a field name is a json name. */ @FunctionalInterface @Immutable diff --git a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java index d6e90b1b4..b3c3468cf 100644 --- a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java +++ b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeTest.java @@ -15,16 +15,18 @@ package dev.cel.common.types; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(TestParameterInjector.class) public final class ProtoMessageTypeTest { private static final ImmutableMap FIELD_MAP = @@ -42,7 +44,7 @@ public final class ProtoMessageTypeTest { @Before public void setUp() { testMessage = - new ProtoMessageType( + ProtoMessageType.create( "my.package.TestMessage", FIELD_MAP.keySet(), (field) -> Optional.ofNullable(FIELD_MAP.get(field)), @@ -75,4 +77,89 @@ public void findExtension() { ProtoMessageType.Extension.of(extName, EXTENSION_MAP.get(extName), testMessage)); } } + + @Test + public void createWithUnenumerableFields_findField_delegatesToResolver( + @TestParameter({"bool_value", "int_value", "string_value", "map_value"}) String fieldName) { + ProtoMessageType unenumerableMessage = newUnenumerableTestMessage(); + + assertThat(unenumerableMessage.findField(fieldName)) + .hasValue(StructType.Field.of(fieldName, FIELD_MAP.get(fieldName))); + } + + @Test + public void createWithUnenumerableFields_findField_undefinedField_returnsEmpty() { + ProtoMessageType unenumerableMessage = newUnenumerableTestMessage(); + + assertThat(unenumerableMessage.findField("undefined_field")).isEmpty(); + } + + @Test + public void createWithUnenumerableFields_findExtension_delegatesToResolver() { + ProtoMessageType unenumerableMessage = newUnenumerableTestMessage(); + + assertThat( + unenumerableMessage + .findExtension("my.package.int_extension") + .map(ProtoMessageType.Extension::type)) + .hasValue(SimpleType.INT); + } + + @Test + public void createWithUnenumerableFields_findExtension_undefinedExtension_returnsEmpty() { + ProtoMessageType unenumerableMessage = newUnenumerableTestMessage(); + + assertThat(unenumerableMessage.findExtension("my.package.undefined_extension")).isEmpty(); + } + + @Test + public void createWithUnenumerableFields_fieldNames_throws() { + ProtoMessageType unenumerableMessage = newUnenumerableTestMessage(); + + assertThrows(IllegalStateException.class, unenumerableMessage::fieldNames); + } + + @Test + public void createWithUnenumerableFields_fields_throws() { + ProtoMessageType unenumerableMessage = newUnenumerableTestMessage(); + + assertThrows(IllegalStateException.class, unenumerableMessage::fields); + } + + @Test + public void createWithUnenumerableFields_withVisibleFields_becomesEnumerable() { + ProtoMessageType maskedMessage = + newUnenumerableTestMessage().withVisibleFields(ImmutableSet.of("bool_value")); + + assertThat(maskedMessage.fieldNames()).containsExactly("bool_value"); + assertThat(maskedMessage.findField("int_value")).isEmpty(); + } + + @Test + public void createWithUnenumerableFields_withJsonNameResolver_resolvesJsonName() { + ProtoMessageType message = + ProtoMessageType.createWithUnenumerableFields( + "my.package.TestMessage", + (field) -> Optional.ofNullable(FIELD_MAP.get(field)), + (extension) -> Optional.ofNullable(EXTENSION_MAP.get(extension)), + "boolValue"::equals); + + assertThat(message.isJsonName("boolValue")).isTrue(); + assertThat(message.isJsonName("bool_value")).isFalse(); + } + + @Test + public void createWithUnenumerableFields_defaultJsonNameResolver_returnsFalse() { + ProtoMessageType message = newUnenumerableTestMessage(); + + assertThat(message.isJsonName("boolValue")).isFalse(); + assertThat(message.isJsonName("bool_value")).isFalse(); + } + + private static ProtoMessageType newUnenumerableTestMessage() { + return ProtoMessageType.createWithUnenumerableFields( + "my.package.TestMessage", + (field) -> Optional.ofNullable(FIELD_MAP.get(field)), + (extension) -> Optional.ofNullable(EXTENSION_MAP.get(extension))); + } }