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..6755d4b88 100644 --- a/checker/src/main/java/dev/cel/checker/BUILD.bazel +++ b/checker/src/main/java/dev/cel/checker/BUILD.bazel @@ -30,23 +30,28 @@ CHECKER_LEGACY_ENV_SOURCES = [ "ExprChecker.java", "ExprVisitor.java", "InferenceContext.java", + "LegacyTypeProviderBridge.java", "TypeFormatter.java", + "TypeInference.java", + "TypeProvider.java", ] java_library( name = "type_provider_legacy", srcs = [ "DescriptorTypeProvider.java", - "TypeProvider.java", "Types.java", ], tags = [ ], + exports = [ + ":checker_legacy_environment", + ], deps = [ + ":checker_legacy_environment", "//:auto_value", "//common/annotations", "//common/internal:file_descriptor_converter", - "//common/types", "//common/types:cel_proto_types", "//common/types:type_providers", "@cel_spec//proto/cel/expr:checked_java_proto", @@ -129,24 +134,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, @@ -154,7 +141,6 @@ java_library( ], deps = [ ":standard_decl", - ":type_provider_legacy", "//:auto_value", "//common:cel_ast", "//common:cel_function_decl", 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..5da5e0380 100644 --- a/checker/src/main/java/dev/cel/checker/Env.java +++ b/checker/src/main/java/dev/cel/checker/Env.java @@ -21,6 +21,7 @@ import dev.cel.expr.Type; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -84,13 +85,27 @@ public class Env { CelFunctionDecl.newBuilder().setName("*error*").build(); private static final CelTypeProvider EMPTY_TYPE_PROVIDER = - new CelTypeProvider.CombinedCelTypeProvider(ImmutableList.of()); + new CelTypeProvider() { + @Override + public ImmutableCollection types() { + return ImmutableList.of(); + } + + @Override + public Optional findType(String typeName) { + return Optional.empty(); + } + }; + + private static final CelOptions LEGACY_TYPE_CHECKER_OPTIONS = + CelOptions.newBuilder() + .disableCelStandardEquality(false) + .enableNamespacedDeclarations(false) + .build(); /** 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. @@ -109,12 +124,6 @@ public class Env { /** CEL Feature flags. */ private final CelOptions celOptions; - private static final CelOptions LEGACY_TYPE_CHECKER_OPTIONS = - CelOptions.newBuilder() - .disableCelStandardEquality(false) - .enableNamespacedDeclarations(false) - .build(); - /** * @deprecated Do not use. This exists for compatibility reasons. Migrate to CEL-Java fluent APIs. * See {@code CelCompilerFactory}. @@ -130,15 +139,15 @@ public static Env unconfigured(Errors errors) { */ @VisibleForTesting static Env unconfigured(Errors errors, CelOptions celOptions) { - return unconfigured(errors, new DescriptorTypeProvider(), celOptions); + return unconfigured(errors, EMPTY_TYPE_PROVIDER, 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 +159,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); } /** @@ -159,7 +168,7 @@ public static Env unconfigured(Errors errors, TypeProvider typeProvider, CelOpti */ @Deprecated public static Env standard(Errors errors) { - return standard(errors, new DescriptorTypeProvider()); + return standard(errors, EMPTY_TYPE_PROVIDER, LEGACY_TYPE_CHECKER_OPTIONS); } /** @@ -171,14 +180,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 +197,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 +220,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 +268,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 +515,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 +539,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 +551,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; } /** @@ -705,22 +706,23 @@ private Env addFunction(CelFunctionDecl decl) { */ private void addOverload(CelFunctionDecl.Builder builder, CelOverloadDecl overload) { // Compute the type of the overload with all type parameters replaced by DYN. - // We are using a property of Types.substitute which replaces all unbound type + // We are using a property of TypeInference.substitute which replaces all unbound type // parameters by DYN. ImmutableMap emptySubs = ImmutableMap.of(); CelType overloadFunction = CelTypes.createFunctionType(overload.resultType(), overload.parameterTypes()); - CelType overloadTypeErased = Types.substitute(emptySubs, overloadFunction, true); + CelType overloadTypeErased = TypeInference.substitute(emptySubs, overloadFunction, true); // Loop over existing overloads to find any overlap. for (CelOverloadDecl existing : builder.overloads()) { CelType existingFunction = CelTypes.createFunctionType(existing.resultType(), existing.parameterTypes()); - CelType existingTypeErased = Types.substitute(emptySubs, existingFunction, true); + CelType existingTypeErased = TypeInference.substitute(emptySubs, existingFunction, true); boolean overlap = - Types.isAssignable(emptySubs, overloadTypeErased, existingTypeErased) != null - || Types.isAssignable(emptySubs, existingTypeErased, overloadTypeErased) != null; + TypeInference.isAssignable(emptySubs, overloadTypeErased, existingTypeErased) != null + || TypeInference.isAssignable(emptySubs, existingTypeErased, overloadTypeErased) + != null; if (overlap && existing.isInstanceFunction() == overload.isInstanceFunction()) { reportError( /* exprId= */ 0, @@ -1081,15 +1083,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..b99634246 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; @@ -628,7 +643,7 @@ private OverloadResolution resolveOverload( // More than one matching overload in non-strict mode, narrow result type to DYN unless // the overload type matches the previous result type. CelType fnResultType = inferenceContext.specialize(overloadType).parameters().get(0); - if (!Types.isDyn(resultType) && !resultType.equals(fnResultType)) { + if (!TypeInference.isDyn(resultType) && !resultType.equals(fnResultType)) { // TODO: Consider joining result types of successful candidates when the // types are assignable, but not the same. Note, type assignability checks here seem to // mutate the type substitutions list in unexpected ways that result in errant results. @@ -677,7 +692,7 @@ private CelType visitSelectField( operandType = unwrapOptional(operandType); } - if (!Types.isDynOrError(operandType)) { + if (!TypeInference.isDynOrError(operandType)) { if (operandType.kind().equals(CelKind.STRUCT)) { CelType fieldType = getFieldType(expr.id(), getPosition(expr), operandType, field); if (!fieldType.equals(SimpleType.ERROR)) { @@ -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) { @@ -886,7 +864,7 @@ private CelType joinTypes(long exprId, int position, CelType previousType, CelTy } else if (!inferenceContext.isAssignable(previousType, type)) { return SimpleType.DYN; } - return Types.mostGeneral(previousType, type); + return TypeInference.mostGeneral(previousType, type); } private void assertIsAssignable(long exprId, int position, CelType actual, CelType expected) { @@ -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/InferenceContext.java b/checker/src/main/java/dev/cel/checker/InferenceContext.java index c254736ef..2444fe22f 100644 --- a/checker/src/main/java/dev/cel/checker/InferenceContext.java +++ b/checker/src/main/java/dev/cel/checker/InferenceContext.java @@ -56,7 +56,7 @@ public CelType newInstance(Iterable typeParams, CelType type) { for (String typeParam : typeParams) { subs.put(TypeParamType.create(typeParam), newTypeVar(typeParam)); } - return Types.substitute(subs, type, false); + return TypeInference.substitute(subs, type, false); } /** @@ -66,7 +66,7 @@ public CelType newInstance(Iterable typeParams, CelType type) { */ @CanIgnoreReturnValue public boolean isAssignable(CelType type1, CelType type2) { - Map newSubs = Types.isAssignable(substitution, type1, type2); + Map newSubs = TypeInference.isAssignable(substitution, type1, type2); if (newSubs != null) { substitution = newSubs; return true; @@ -77,7 +77,7 @@ public boolean isAssignable(CelType type1, CelType type2) { /** Same as {@link #isAssignable(CelType, CelType)} for lists of types. */ public boolean isAssignable(List list1, List list2) { - Map newSubs = Types.isAssignable(substitution, list1, list2); + Map newSubs = TypeInference.isAssignable(substitution, list1, list2); if (newSubs != null) { substitution = newSubs; return true; @@ -88,7 +88,7 @@ public boolean isAssignable(List list1, List list2) { /** Specializes the given type using the substitution of this context. */ public CelType specialize(CelType type) { - return Types.substitute(substitution, type, false); + return TypeInference.substitute(substitution, type, false); } /** Specializes using given type list of types using the substitution of this context. */ @@ -105,6 +105,6 @@ public List specialize(List types) { * type parameters to DYN. */ public CelType finalize(CelType type) { - return Types.substitute(substitution, type, true); + return TypeInference.substitute(substitution, type, true); } } 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/TypeFormatter.java b/checker/src/main/java/dev/cel/checker/TypeFormatter.java index 19db6b34d..f6139ee4f 100644 --- a/checker/src/main/java/dev/cel/checker/TypeFormatter.java +++ b/checker/src/main/java/dev/cel/checker/TypeFormatter.java @@ -56,7 +56,7 @@ static String formatFunction( * is useful for computing overload signatures. * *

When {@code typeParamToDyn} is {@code true}, parameterized type argument are represented as - * {@code Types.DYN} values. + * {@code SimpleType.DYN} values. */ static String formatFunction( @Nullable CelType resultType, diff --git a/checker/src/main/java/dev/cel/checker/TypeInference.java b/checker/src/main/java/dev/cel/checker/TypeInference.java new file mode 100644 index 000000000..0a05790fe --- /dev/null +++ b/checker/src/main/java/dev/cel/checker/TypeInference.java @@ -0,0 +1,378 @@ +// 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.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import dev.cel.common.types.CelKind; +import dev.cel.common.types.CelType; +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.TypeType; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +/** + * Type inference, unification, and assignability algorithms for {@link CelType}. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +@CheckReturnValue +final class TypeInference { + + static boolean isDynOrError(CelType type) { + checkNotNull(type, "type"); + switch (type.kind()) { + case ERROR: + return true; + default: + return isDyn(type); + } + } + + static boolean isDyn(CelType type) { + checkNotNull(type, "type"); + switch (type.kind()) { + case DYN: + case ANY: + return true; + default: + return false; + } + } + + private static boolean isTypeParam(CelType type) { + return type.kind().equals(CelKind.TYPE_PARAM); + } + + private static boolean hasTypeParam(CelType type) { + if (isTypeParam(type)) { + return true; + } + if (type instanceof NullableType) { + return hasTypeParam(((NullableType) type).targetType()); + } + for (CelType param : type.parameters()) { + if (hasTypeParam(param)) { + return true; + } + } + return false; + } + + static CelType mostGeneral(CelType type1, CelType type2) { + checkNotNull(type1, "type1"); + checkNotNull(type2, "type2"); + return isEqualOrLessSpecific(type1, type2) ? type1 : type2; + } + + static @Nullable Map isAssignable( + Map subs, CelType type1, CelType type2) { + checkNotNull(subs, "subs"); + checkNotNull(type1, "type1"); + checkNotNull(type2, "type2"); + Map subsCopy = new HashMap<>(subs); + if (internalIsAssignable(subsCopy, type1, type2)) { + return subsCopy; + } + return null; + } + + static @Nullable Map isAssignable( + Map subs, List list1, List list2) { + checkNotNull(subs, "subs"); + checkNotNull(list1, "list1"); + checkNotNull(list2, "list2"); + Map subsCopy = new HashMap<>(subs); + if (internalIsAssignable(subsCopy, list1, list2)) { + return subsCopy; + } + return null; + } + + private static boolean internalIsAssignable( + Map subs, CelType type1, CelType type2) { + // A type is always assignable to itself. + // Early terminate the call to avoid cases of infinite recursion. + if (type1.equals(type2)) { + return true; + } + // Process type parameters. + if (isTypeParam(type2)) { + if (subs.containsKey(type2)) { + CelType t2Sub = subs.get(type2); + // Continue regular process with the assignment for type2. + if (!internalIsAssignable(subs, type1, t2Sub)) { + return false; + } + CelType t2New = mostGeneral(type1, t2Sub); + if (notReferencedIn(subs, type2, t2New)) { + subs.put(type2, t2New); + } + return true; + } + if (notReferencedIn(subs, type2, type1)) { + subs.put(type2, type1); + return true; + } + } + if (isTypeParam(type1)) { + if (subs.containsKey(type1)) { + CelType t1Sub = subs.get(type1); + // Continue regular process with the assignment for type1. + if (!internalIsAssignable(subs, t1Sub, type2)) { + return false; + } + CelType t1New = mostGeneral(t1Sub, type2); + if (notReferencedIn(subs, type1, t1New)) { + subs.put(type1, t1New); + } + return true; + } + if (notReferencedIn(subs, type1, type2)) { + subs.put(type1, type2); + return true; + } + } + // Next check for wildcard types. + if (isDynOrError(type1) || isDynOrError(type2)) { + return true; + } + + // Preserve the nullness checks of the legacy type-checker. + if (type1.kind() == CelKind.NULL_TYPE) { + return isAssignableFromNull(type2); + } + if (type2.kind() == CelKind.NULL_TYPE) { + return isAssignableFromNull(type1); + } + + if (type1.kind() != type2.kind()) { + return false; + } + + switch (type1.kind()) { + case TYPE: + if (!(type1 instanceof TypeType) || !(type2 instanceof TypeType)) { + return type2.isAssignableFrom(type1); + } + TypeType fromType = (TypeType) type1; + TypeType toType = (TypeType) type2; + // If either type contains a type parameter (e.g., type(T) in foo(data, type(T)) -> T), + // delegate to inner type unification to bind or validate type parameter substitutions. + // Returns true if the inner types structurally match, unify with an unbound type param, + // or conform to an existing binding in 'subs'. Returns false on structural/kind mismatches + // (e.g., int vs list(T)), occurs-check cycles, or conflicting type param bindings. + + if (hasTypeParam(fromType.type()) || hasTypeParam(toType.type())) { + return internalIsAssignable(subs, fromType.type(), toType.type()); + } + // Concrete types are coassignable in CEL (e.g., type(1) == type("a"), type([1]) == list). + return true; + case OPAQUE: + case LIST: + case MAP: + return internalIsCandidateAssignableToTarget(subs, type1, type2); + default: + return type2.isAssignableFrom(type1); + } + } + + private static boolean internalIsAssignable( + Map subs, List list1, List list2) { + if (list1.size() != list2.size()) { + return false; + } + int i = 0; + for (CelType type : list1) { + if (!internalIsAssignable(subs, type, list2.get(i++))) { + return false; + } + } + return true; + } + + private static boolean internalIsCandidateAssignableToTarget( + Map subs, CelType candidate, CelType target) { + return candidate.name().equals(target.name()) + && internalIsAssignable(subs, candidate.parameters(), target.parameters()); + } + + private static boolean isAssignableFromNull(CelType targetType) { + switch (targetType.kind()) { + case OPAQUE: + case STRUCT: + case DURATION: + case TIMESTAMP: + return true; + default: + return targetType.isAssignableFrom(SimpleType.NULL_TYPE); + } + } + + static boolean isEqualOrLessSpecific(CelType type1, CelType type2) { + checkNotNull(type1, "type1"); + checkNotNull(type2, "type2"); + // The first type is less specific. + if (isDyn(type1) || isTypeParam(type1)) { + return true; + } + // The first type is not less specific. + if (isDyn(type2) || isTypeParam(type2)) { + return false; + } + if (type1 instanceof NullableType && type2 instanceof NullableType) { + return isEqualOrLessSpecific( + ((NullableType) type1).targetType(), ((NullableType) type2).targetType()); + } + if (type1 instanceof NullableType || type2 instanceof NullableType) { + return false; + } + // Types must be of the same kind to be equal. + if (type1.kind() != type2.kind()) { + return false; + } + + // With limited exceptions for ANY and JSON values, the types must agree and be equivalent in + // order to return true. + switch (type1.kind()) { + case OPAQUE: + case LIST: + case MAP: + // Both types must have the same kind and have the same name in order to be equal or less + // specific. + if (!type1.kind().equals(type2.kind())) { + return false; + } + if (!type1.name().equals(type2.name())) { + return false; + } + return isEqualOrLessSpecific(type1.parameters(), type2.parameters()); + case TYPE: + // Type values must have equal or less specific internal types. + if (!(type1 instanceof TypeType) || !(type2 instanceof TypeType)) { + return type1.equals(type2); + } + TypeType typeType1 = (TypeType) type1; + TypeType typeType2 = (TypeType) type2; + return isEqualOrLessSpecific(typeType1.type(), typeType2.type()); + + // Message, primitive, well-known, and wrapper type names must be equal to be equivalent. + default: + return type1.equals(type2); + } + } + + private static boolean isEqualOrLessSpecific(List types1, List types2) { + if (types1.size() != types2.size()) { + return false; + } + for (int i = 0; i < types1.size(); i++) { + if (!isEqualOrLessSpecific(types1.get(i), types2.get(i))) { + return false; + } + } + return true; + } + + private static boolean notReferencedIn( + Map subs, CelType type, CelType withinType) { + if (type.equals(withinType)) { + return false; + } + + if (withinType instanceof NullableType) { + return notReferencedIn(subs, type, ((NullableType) withinType).targetType()); + } + + switch (withinType.kind()) { + case TYPE_PARAM: + return !subs.containsKey(withinType) || notReferencedIn(subs, type, subs.get(withinType)); + case OPAQUE: + for (CelType typeArg : withinType.parameters()) { + if (!notReferencedIn(subs, type, typeArg)) { + return false; + } + } + return true; + case LIST: + ListType listType = (ListType) withinType; + return notReferencedIn(subs, type, listType.elemType()); + case MAP: + MapType mapType = (MapType) withinType; + return notReferencedIn(subs, type, mapType.keyType()) + && notReferencedIn(subs, type, mapType.valueType()); + case TYPE: + TypeType typeType = (TypeType) withinType; + return notReferencedIn(subs, type, typeType.type()); + default: + return true; + } + } + + static CelType substitute(Map subs, CelType type, boolean typeParamToDyn) { + checkNotNull(subs, "subs"); + checkNotNull(type, "type"); + if (subs.containsKey(type)) { + return substitute(subs, subs.get(type), typeParamToDyn); + } + if (type instanceof NullableType) { + return NullableType.create( + substitute(subs, ((NullableType) type).targetType(), typeParamToDyn)); + } + if (typeParamToDyn && isTypeParam(type)) { + return SimpleType.DYN; + } + switch (type.kind()) { + case OPAQUE: + ImmutableList.Builder parameterTypes = ImmutableList.builder(); + for (int i = 0; i < type.parameters().size(); i++) { + parameterTypes.add(substitute(subs, type.parameters().get(i), typeParamToDyn)); + } + + if (type instanceof OptionalType) { + return OptionalType.create(parameterTypes.build().get(0)); + } + return OpaqueType.create(type.name()).withParameters(parameterTypes.build()); + case LIST: + ListType listType = (ListType) type; + return ListType.create(substitute(subs, listType.elemType(), typeParamToDyn)); + case MAP: + MapType mapType = (MapType) type; + return MapType.create( + substitute(subs, mapType.keyType(), typeParamToDyn), + substitute(subs, mapType.valueType(), typeParamToDyn)); + case TYPE: + TypeType newType = (TypeType) type; + return TypeType.create(substitute(subs, newType.type(), typeParamToDyn)); + default: + return type; + } + } + + private TypeInference() {} +} 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/main/java/dev/cel/checker/Types.java b/checker/src/main/java/dev/cel/checker/Types.java index f9b82ecb7..2219961d6 100644 --- a/checker/src/main/java/dev/cel/checker/Types.java +++ b/checker/src/main/java/dev/cel/checker/Types.java @@ -19,22 +19,13 @@ import dev.cel.expr.Type.TypeKindCase; import dev.cel.expr.Type.WellKnownType; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.protobuf.DescriptorProtos.FieldDescriptorProto; import com.google.protobuf.Empty; import com.google.protobuf.NullValue; import dev.cel.common.annotations.Internal; -import dev.cel.common.types.CelKind; import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; -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.TypeType; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -182,45 +173,16 @@ public static boolean isDynOrError(Type type) { /** Tests whether the type has error or dyn kind. Both have the property to match any type. */ public static boolean isDynOrError(CelType type) { - switch (type.kind()) { - case ERROR: - return true; - default: - return isDyn(type); - } + return TypeInference.isDynOrError(type); } public static boolean isDyn(CelType type) { - switch (type.kind()) { - case DYN: - case ANY: - return true; - default: - return false; - } - } - - /** Tests whether the {@code type} is a type param. */ - private static boolean isTypeParam(CelType type) { - return type.kind().equals(CelKind.TYPE_PARAM); - } - - /** Tests whether the {@code type} contains any type params directly or transitively. */ - private static boolean hasTypeParam(CelType type) { - if (isTypeParam(type)) { - return true; - } - for (CelType param : type.parameters()) { - if (hasTypeParam(param)) { - return true; - } - } - return false; + return TypeInference.isDyn(type); } /** Returns the more general of two types which are known to unify. */ public static CelType mostGeneral(CelType type1, CelType type2) { - return isEqualOrLessSpecific(type1, type2) ? type1 : type2; + return TypeInference.mostGeneral(type1, type2); } /** @@ -230,11 +192,7 @@ public static CelType mostGeneral(CelType type1, CelType type2) { */ public static @Nullable Map isAssignable( Map subs, CelType type1, CelType type2) { - Map subsCopy = new HashMap<>(subs); - if (internalIsAssignable(subsCopy, type1, type2)) { - return subsCopy; - } - return null; + return TypeInference.isAssignable(subs, type1, type2); } /** @@ -256,9 +214,11 @@ public static CelType mostGeneral(CelType type1, CelType type2) { (prev, next) -> next, HashMap::new)); - if (internalIsAssignable( - subsCopy, CelProtoTypes.typeToCelType(type1), CelProtoTypes.typeToCelType(type2))) { - return subsCopy.entrySet().stream() + Map result = + TypeInference.isAssignable( + subsCopy, CelProtoTypes.typeToCelType(type1), CelProtoTypes.typeToCelType(type2)); + if (result != null) { + return result.entrySet().stream() .collect( Collectors.toMap( k -> CelProtoTypes.celTypeToType(k.getKey()), @@ -275,131 +235,7 @@ public static CelType mostGeneral(CelType type1, CelType type2) { */ public static @Nullable Map isAssignable( Map subs, List list1, List list2) { - Map subsCopy = new HashMap<>(subs); - if (internalIsAssignable(subsCopy, list1, list2)) { - return subsCopy; - } - return null; - } - - private static boolean internalIsAssignable( - Map subs, CelType type1, CelType type2) { - // A type is always assignable to itself. - // Early terminate the call to avoid cases of infinite recursion. - if (type1.equals(type2)) { - return true; - } - // Process type parameters. - if (isTypeParam(type2)) { - if (subs.containsKey(type2)) { - CelType t2Sub = subs.get(type2); - // Continue regular process with the assignment for type2. - if (!internalIsAssignable(subs, type1, t2Sub)) { - return false; - } - CelType t2New = mostGeneral(type1, t2Sub); - if (notReferencedIn(subs, type2, t2New)) { - subs.put(type2, t2New); - } - return true; - } - if (notReferencedIn(subs, type2, type1)) { - subs.put(type2, type1); - return true; - } - } - if (isTypeParam(type1)) { - if (subs.containsKey(type1)) { - CelType t1Sub = subs.get(type1); - // Continue regular process with the assignment for type1. - if (!internalIsAssignable(subs, t1Sub, type2)) { - return false; - } - CelType t1New = mostGeneral(t1Sub, type2); - if (notReferencedIn(subs, type1, t1New)) { - subs.put(type1, t1New); - } - return true; - } - if (notReferencedIn(subs, type1, type2)) { - subs.put(type1, type2); - return true; - } - } - // Next check for wildcard types. - if (isDynOrError(type1) || isDynOrError(type2)) { - return true; - } - - // Preserve the nullness checks of the legacy type-checker. - if (type1.kind() == CelKind.NULL_TYPE) { - return isAssignableFromNull(type2); - } - if (type2.kind() == CelKind.NULL_TYPE) { - return isAssignableFromNull(type1); - } - - if (type1.kind() != type2.kind()) { - return false; - } - - switch (type1.kind()) { - case TYPE: - if (!(type1 instanceof TypeType) || !(type2 instanceof TypeType)) { - return type2.isAssignableFrom(type1); - } - TypeType fromType = (TypeType) type1; - TypeType toType = (TypeType) type2; - // If either type contains a type parameter (e.g., type(T) in foo(data, type(T)) -> T), - // delegate to inner type unification to bind or validate type parameter substitutions. - // Returns true if the inner types structurally match, unify with an unbound type param, - // or conform to an existing binding in 'subs'. Returns false on structural/kind mismatches - // (e.g., int vs list(T)), occurs-check cycles, or conflicting type param bindings. - - if (hasTypeParam(fromType.type()) || hasTypeParam(toType.type())) { - return internalIsAssignable(subs, fromType.type(), toType.type()); - } - // Concrete types are coassignable in CEL (e.g., type(1) == type("a"), type([1]) == list). - return true; - case OPAQUE: - case LIST: - case MAP: - return internalIsCandidateAssignableToTarget(subs, type1, type2); - default: - return type2.isAssignableFrom(type1); - } - } - - private static boolean internalIsAssignable( - Map subs, List list1, List list2) { - if (list1.size() != list2.size()) { - return false; - } - int i = 0; - for (CelType type : list1) { - if (!internalIsAssignable(subs, type, list2.get(i++))) { - return false; - } - } - return true; - } - - private static boolean internalIsCandidateAssignableToTarget( - Map subs, CelType candidate, CelType target) { - return candidate.name().equals(target.name()) - && internalIsAssignable(subs, candidate.parameters(), target.parameters()); - } - - private static boolean isAssignableFromNull(CelType targetType) { - switch (targetType.kind()) { - case OPAQUE: - case STRUCT: - case DURATION: - case TIMESTAMP: - return true; - default: - return targetType.isAssignableFrom(SimpleType.NULL_TYPE); - } + return TypeInference.isAssignable(subs, list1, list2); } /** @@ -419,95 +255,7 @@ public static boolean isEqualOrLessSpecific(Type type1, Type type2) { * it matches the other type using the DYN type. */ public static boolean isEqualOrLessSpecific(CelType type1, CelType type2) { - // The first type is less specific. - if (isDyn(type1) || isTypeParam(type1)) { - return true; - } - // The first type is not less specific. - if (isDyn(type2) || isTypeParam(type2)) { - return false; - } - // Types must be of the same kind to be equal. - if (type1.kind() != type2.kind()) { - return false; - } - - // With limited exceptions for ANY and JSON values, the types must agree and be equivalent in - // order to return true. - switch (type1.kind()) { - case OPAQUE: - case LIST: - case MAP: - // Both types must have the same kind and have the same name in order to be equal or less - // specific. - if (!type1.kind().equals(type2.kind())) { - return false; - } - if (!type1.name().equals(type2.name())) { - return false; - } - return isEqualOrLessSpecific(type1.parameters(), type2.parameters()); - case TYPE: - // Type values must have equal or less specific internal types. - TypeType typeType1 = (TypeType) type1; - TypeType typeType2 = (TypeType) type2; - return isEqualOrLessSpecific(typeType1.type(), typeType2.type()); - - // Message, primitive, well-known, and wrapper type names must be equal to be equivalent. - default: - return type1.equals(type2); - } - } - - private static boolean isEqualOrLessSpecific(List types1, List types2) { - if (types1.size() != types2.size()) { - return false; - } - for (int i = 0; i < types1.size(); i++) { - if (!isEqualOrLessSpecific(types1.get(i), types2.get(i))) { - return false; - } - } - return true; - } - - /** - * Check whether the type doesn't appear directly or transitively within other type. This is a - * standard requirement for type unification, commonly referred to as the "occurs check". - */ - private static boolean notReferencedIn( - Map subs, CelType type, CelType withinType) { - if (type.equals(withinType)) { - return false; - } - - if (withinType instanceof NullableType) { - return notReferencedIn(subs, type, ((NullableType) withinType).targetType()); - } - - switch (withinType.kind()) { - case TYPE_PARAM: - return !subs.containsKey(withinType) || notReferencedIn(subs, type, subs.get(withinType)); - case OPAQUE: - for (CelType typeArg : withinType.parameters()) { - if (!notReferencedIn(subs, type, typeArg)) { - return false; - } - } - return true; - case LIST: - ListType listType = (ListType) withinType; - return notReferencedIn(subs, type, listType.elemType()); - case MAP: - MapType mapType = (MapType) withinType; - return notReferencedIn(subs, type, mapType.keyType()) - && notReferencedIn(subs, type, mapType.valueType()); - case TYPE: - TypeType typeType = (TypeType) withinType; - return notReferencedIn(subs, type, typeType.type()); - default: - return true; - } + return TypeInference.isEqualOrLessSpecific(type1, type2); } /** @@ -533,37 +281,7 @@ public static Type substitute(Map subs, Type type, boolean typeParam */ public static CelType substitute( Map subs, CelType type, boolean typeParamToDyn) { - if (subs.containsKey(type)) { - return substitute(subs, subs.get(type), typeParamToDyn); - } - if (typeParamToDyn && isTypeParam(type)) { - return SimpleType.DYN; - } - switch (type.kind()) { - case OPAQUE: - ImmutableList.Builder parameterTypes = new ImmutableList.Builder<>(); - for (int i = 0; i < type.parameters().size(); i++) { - parameterTypes.add(substitute(subs, type.parameters().get(i), typeParamToDyn)); - } - - if (type instanceof OptionalType) { - return OptionalType.create(parameterTypes.build().get(0)); - } - return OpaqueType.create(type.name()).withParameters(parameterTypes.build()); - case LIST: - ListType listType = (ListType) type; - return ListType.create(substitute(subs, listType.elemType(), typeParamToDyn)); - case MAP: - MapType mapType = (MapType) type; - return MapType.create( - substitute(subs, mapType.keyType(), typeParamToDyn), - substitute(subs, mapType.valueType(), typeParamToDyn)); - case TYPE: - TypeType newType = (TypeType) type; - return TypeType.create(substitute(subs, newType.type(), typeParamToDyn)); - default: - return type; - } + return TypeInference.substitute(subs, type, typeParamToDyn); } private Types() {} 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/TypeInferenceTest.java b/checker/src/test/java/dev/cel/checker/TypeInferenceTest.java new file mode 100644 index 000000000..e9cc060ec --- /dev/null +++ b/checker/src/test/java/dev/cel/checker/TypeInferenceTest.java @@ -0,0 +1,434 @@ +// 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 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.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 java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class TypeInferenceTest { + + private enum DynamicTypeTestCase { + DYN(SimpleType.DYN, true), + ANY(SimpleType.ANY, true), + INT(SimpleType.INT, false), + STRING(SimpleType.STRING, false), + BOOL(SimpleType.BOOL, false), + ERROR(SimpleType.ERROR, false); + + final CelType type; + final boolean expectedIsDyn; + + DynamicTypeTestCase(CelType type, boolean expectedIsDyn) { + this.type = type; + this.expectedIsDyn = expectedIsDyn; + } + } + + @Test + public void isDyn_evaluatesCorrectly(@TestParameter DynamicTypeTestCase testCase) { + assertThat(TypeInference.isDyn(testCase.type)).isEqualTo(testCase.expectedIsDyn); + } + + private enum DynOrErrorTestCase { + DYN(SimpleType.DYN, true), + ANY(SimpleType.ANY, true), + ERROR(SimpleType.ERROR, true), + INT(SimpleType.INT, false), + BOOL(SimpleType.BOOL, false); + + final CelType type; + final boolean expectedIsDynOrError; + + DynOrErrorTestCase(CelType type, boolean expectedIsDynOrError) { + this.type = type; + this.expectedIsDynOrError = expectedIsDynOrError; + } + } + + @Test + public void isDynOrError_evaluatesCorrectly(@TestParameter DynOrErrorTestCase testCase) { + assertThat(TypeInference.isDynOrError(testCase.type)).isEqualTo(testCase.expectedIsDynOrError); + } + + @Test + public void mostGeneral_dynWithConcrete_returnsDyn() { + assertThat(TypeInference.mostGeneral(SimpleType.DYN, SimpleType.INT)).isEqualTo(SimpleType.DYN); + assertThat(TypeInference.mostGeneral(SimpleType.INT, SimpleType.DYN)).isEqualTo(SimpleType.DYN); + } + + @Test + public void mostGeneral_typeParamWithConcrete_returnsTypeParam() { + TypeParamType typeParamT = TypeParamType.create("T"); + + assertThat(TypeInference.mostGeneral(typeParamT, SimpleType.STRING)).isEqualTo(typeParamT); + assertThat(TypeInference.mostGeneral(SimpleType.STRING, typeParamT)).isEqualTo(typeParamT); + } + + @Test + public void isAssignable_sameType_succeeds() { + Map subs = new HashMap<>(); + + Map result = TypeInference.isAssignable(subs, SimpleType.INT, SimpleType.INT); + + assertThat(result).isEmpty(); + } + + @Test + public void isAssignable_incompatibleTypes_returnsNull() { + Map subs = new HashMap<>(); + + Map result = + TypeInference.isAssignable(subs, SimpleType.INT, SimpleType.STRING); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeParam_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + + Map result = TypeInference.isAssignable(subs, SimpleType.INT, typeParamT); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeParamSource_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + + Map result = TypeInference.isAssignable(subs, typeParamT, SimpleType.STRING); + + assertThat(result).containsExactly(typeParamT, SimpleType.STRING); + } + + @Test + public void isAssignable_occursCheckCycle_returnsNull() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + ListType listOfT = ListType.create(typeParamT); + + // T = list(T) is a cycle and must be rejected by the occurs-check + Map result = TypeInference.isAssignable(subs, listOfT, typeParamT); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_listType_elementTypesAssignable() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + ListType listInt = ListType.create(SimpleType.INT); + ListType listT = ListType.create(typeParamT); + + Map result = TypeInference.isAssignable(subs, listInt, listT); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_mapType_keyAndValueTypesAssignable() { + Map subs = new HashMap<>(); + TypeParamType typeParamK = TypeParamType.create("K"); + TypeParamType typeParamV = TypeParamType.create("V"); + MapType mapIntString = MapType.create(SimpleType.INT, SimpleType.STRING); + MapType mapKV = MapType.create(typeParamK, typeParamV); + + Map result = TypeInference.isAssignable(subs, mapIntString, mapKV); + + assertThat(result).containsExactly(typeParamK, SimpleType.INT, typeParamV, SimpleType.STRING); + } + + @Test + public void isAssignable_nullTypeToStruct_succeeds() { + Map subs = new HashMap<>(); + CelType structType = StructTypeReference.create("my.Message"); + + Map result = + TypeInference.isAssignable(subs, SimpleType.NULL_TYPE, structType); + + assertThat(result).isEmpty(); + } + + @Test + public void isAssignable_nullTypeToNullable_succeeds() { + Map subs = new HashMap<>(); + CelType nullableInt = NullableType.create(SimpleType.INT); + + Map result = + TypeInference.isAssignable(subs, SimpleType.NULL_TYPE, nullableInt); + + assertThat(result).isEmpty(); + } + + @Test + public void isAssignable_nullTypeToPrimitive_returnsNull() { + Map subs = new HashMap<>(); + + Map result = + TypeInference.isAssignable(subs, SimpleType.NULL_TYPE, SimpleType.INT); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_opaqueType_parametersMatch_succeeds() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + OpaqueType opaque1 = OpaqueType.create("vector", SimpleType.INT); + OpaqueType opaque2 = OpaqueType.create("vector", typeParamT); + + Map result = TypeInference.isAssignable(subs, opaque1, opaque2); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_opaqueType_differentNames_returnsNull() { + Map subs = new HashMap<>(); + OpaqueType opaque1 = OpaqueType.create("vector", SimpleType.INT); + OpaqueType opaque2 = OpaqueType.create("set", SimpleType.INT); + + Map result = TypeInference.isAssignable(subs, opaque1, opaque2); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_concreteTypesCoassignable() { + Map subs = new HashMap<>(); + CelType typeInt = TypeType.create(SimpleType.INT); + CelType typeString = TypeType.create(SimpleType.STRING); + + Map result = TypeInference.isAssignable(subs, typeInt, typeString); + + assertThat(result).isEmpty(); + } + + @Test + public void isAssignable_typeType_parameterizedTypeType_bindsInnerType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType typeOfInt = TypeType.create(SimpleType.INT); + CelType typeOfT = TypeType.create(typeParamT); + + Map result = TypeInference.isAssignable(subs, typeOfInt, typeOfT); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_pairwiseList_succeeds() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + ImmutableList list1 = ImmutableList.of(SimpleType.INT, SimpleType.STRING); + ImmutableList list2 = ImmutableList.of(typeParamT, SimpleType.STRING); + + Map result = TypeInference.isAssignable(subs, list1, list2); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_pairwiseList_differentSizes_returnsNull() { + Map subs = new HashMap<>(); + ImmutableList list1 = ImmutableList.of(SimpleType.INT); + ImmutableList list2 = ImmutableList.of(SimpleType.INT, SimpleType.STRING); + + Map result = TypeInference.isAssignable(subs, list1, list2); + + assertThat(result).isNull(); + } + + @Test + public void substitute_boundTypeParam_replacesWithBinding() { + TypeParamType typeParamT = TypeParamType.create("T"); + Map subs = ImmutableMap.of(typeParamT, SimpleType.INT); + + CelType result = TypeInference.substitute(subs, typeParamT, /* typeParamToDyn= */ false); + + assertThat(result).isEqualTo(SimpleType.INT); + } + + @Test + public void substitute_unboundTypeParam_typeParamToDynTrue_replacesWithDyn() { + TypeParamType typeParamT = TypeParamType.create("T"); + Map subs = ImmutableMap.of(); + + CelType result = TypeInference.substitute(subs, typeParamT, /* typeParamToDyn= */ true); + + assertThat(result).isEqualTo(SimpleType.DYN); + } + + @Test + public void substitute_unboundTypeParam_typeParamToDynFalse_preservesTypeParam() { + TypeParamType typeParamT = TypeParamType.create("T"); + Map subs = ImmutableMap.of(); + + CelType result = TypeInference.substitute(subs, typeParamT, /* typeParamToDyn= */ false); + + assertThat(result).isEqualTo(typeParamT); + } + + @Test + public void substitute_nestedTypes_boundTypeParam_substitutesRecursively() { + TypeParamType typeParamT = TypeParamType.create("T"); + Map subs = ImmutableMap.of(typeParamT, SimpleType.INT); + + ListType listType = ListType.create(typeParamT); + MapType mapType = MapType.create(SimpleType.STRING, typeParamT); + OptionalType optionalType = OptionalType.create(typeParamT); + TypeType typeType = TypeType.create(typeParamT); + OpaqueType opaqueType = OpaqueType.create("custom", typeParamT); + + assertThat(TypeInference.substitute(subs, listType, false)) + .isEqualTo(ListType.create(SimpleType.INT)); + assertThat(TypeInference.substitute(subs, mapType, false)) + .isEqualTo(MapType.create(SimpleType.STRING, SimpleType.INT)); + assertThat(TypeInference.substitute(subs, optionalType, false)) + .isEqualTo(OptionalType.create(SimpleType.INT)); + assertThat(TypeInference.substitute(subs, typeType, false)) + .isEqualTo(TypeType.create(SimpleType.INT)); + assertThat(TypeInference.substitute(subs, opaqueType, false)) + .isEqualTo(OpaqueType.create("custom", SimpleType.INT)); + } + + @Test + public void substitute_nestedTypes_unboundTypeParam_typeParamToDynTrue_substitutesDyn() { + TypeParamType typeParamT = TypeParamType.create("T"); + Map subs = ImmutableMap.of(); + + ListType listType = ListType.create(typeParamT); + MapType mapType = MapType.create(SimpleType.STRING, typeParamT); + OptionalType optionalType = OptionalType.create(typeParamT); + + assertThat(TypeInference.substitute(subs, listType, /* typeParamToDyn= */ true)) + .isEqualTo(ListType.create(SimpleType.DYN)); + assertThat(TypeInference.substitute(subs, mapType, /* typeParamToDyn= */ true)) + .isEqualTo(MapType.create(SimpleType.STRING, SimpleType.DYN)); + assertThat(TypeInference.substitute(subs, optionalType, /* typeParamToDyn= */ true)) + .isEqualTo(OptionalType.create(SimpleType.DYN)); + } + + @Test + public void substitute_nullableType_boundTypeParam_substitutesInnerType() { + TypeParamType typeParamT = TypeParamType.create("T"); + TypeParamType typeParamK = TypeParamType.create("K"); + TypeParamType typeParamV = TypeParamType.create("V"); + Map subs = + ImmutableMap.of( + typeParamT, SimpleType.INT, typeParamK, SimpleType.INT, typeParamV, SimpleType.STRING); + + NullableType nullableTypeParam = NullableType.create(typeParamT); + NullableType nullableList = NullableType.create(ListType.create(typeParamT)); + NullableType nullableMap = NullableType.create(MapType.create(typeParamK, typeParamV)); + NullableType nullableTypeType = NullableType.create(TypeType.create(typeParamT)); + + assertThat(TypeInference.substitute(subs, nullableTypeParam, false)) + .isEqualTo(NullableType.create(SimpleType.INT)); + assertThat(TypeInference.substitute(subs, nullableList, false)) + .isEqualTo(NullableType.create(ListType.create(SimpleType.INT))); + assertThat(TypeInference.substitute(subs, nullableMap, false)) + .isEqualTo(NullableType.create(MapType.create(SimpleType.INT, SimpleType.STRING))); + assertThat(TypeInference.substitute(subs, nullableTypeType, false)) + .isEqualTo(NullableType.create(TypeType.create(SimpleType.INT))); + } + + @Test + public void substitute_nullableType_unboundTypeParam_typeParamToDynTrue_substitutesDyn() { + TypeParamType typeParamT = TypeParamType.create("T"); + Map subs = ImmutableMap.of(); + NullableType nullableTypeParam = NullableType.create(typeParamT); + NullableType nullableList = NullableType.create(ListType.create(typeParamT)); + + assertThat(TypeInference.substitute(subs, nullableTypeParam, /* typeParamToDyn= */ true)) + .isEqualTo(NullableType.create(SimpleType.DYN)); + assertThat(TypeInference.substitute(subs, nullableList, /* typeParamToDyn= */ true)) + .isEqualTo(NullableType.create(ListType.create(SimpleType.DYN))); + } + + @Test + public void isAssignable_typeType_typeParamInSource_bindsInnerType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType typeOfT = TypeType.create(typeParamT); + CelType typeOfInt = TypeType.create(SimpleType.INT); + + Map result = TypeInference.isAssignable(subs, typeOfT, typeOfInt); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isEqualOrLessSpecific_evaluatesCorrectly() { + TypeParamType typeParamT = TypeParamType.create("T"); + CelType nullableInt = NullableType.create(SimpleType.INT); + CelType nullableT = NullableType.create(typeParamT); + + assertThat(TypeInference.isEqualOrLessSpecific(SimpleType.DYN, SimpleType.INT)).isTrue(); + assertThat(TypeInference.isEqualOrLessSpecific(SimpleType.INT, SimpleType.DYN)).isFalse(); + assertThat(TypeInference.isEqualOrLessSpecific(typeParamT, SimpleType.INT)).isTrue(); + assertThat(TypeInference.isEqualOrLessSpecific(SimpleType.INT, typeParamT)).isFalse(); + assertThat(TypeInference.isEqualOrLessSpecific(SimpleType.INT, SimpleType.STRING)).isFalse(); + assertThat(TypeInference.isEqualOrLessSpecific(nullableInt, nullableInt)).isTrue(); + assertThat(TypeInference.isEqualOrLessSpecific(nullableT, nullableInt)).isTrue(); + assertThat(TypeInference.isEqualOrLessSpecific(nullableInt, nullableT)).isFalse(); + assertThat(TypeInference.isEqualOrLessSpecific(nullableInt, SimpleType.INT)).isFalse(); + assertThat(TypeInference.isEqualOrLessSpecific(SimpleType.INT, nullableInt)).isFalse(); + assertThat( + TypeInference.isEqualOrLessSpecific( + ListType.create(typeParamT), ListType.create(SimpleType.INT))) + .isTrue(); + assertThat( + TypeInference.isEqualOrLessSpecific( + ListType.create(SimpleType.INT), ListType.create(typeParamT))) + .isFalse(); + assertThat( + TypeInference.isEqualOrLessSpecific( + TypeType.create(typeParamT), TypeType.create(SimpleType.INT))) + .isTrue(); + } + + @Test + public void isEqualOrLessSpecific_nullArguments_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> TypeInference.isEqualOrLessSpecific(null, SimpleType.INT)); + assertThrows( + NullPointerException.class, + () -> TypeInference.isEqualOrLessSpecific(SimpleType.INT, 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))); + } }