From 115bee8ce4c2883b861e4f60035ca19af1ae5583 Mon Sep 17 00:00:00 2001 From: Soy Authors Date: Sun, 3 May 2026 08:50:33 -0700 Subject: [PATCH] Support Proto OneOf Cases in Soy. PiperOrigin-RevId: 909590253 --- .../template/soy/data/SoyProtoValue.java | 32 ++++- .../soy/exprtree/ProtoEnumValueNode.java | 14 +- .../idomsrc/IdomTranslateExprNodeVisitor.java | 18 ++- .../soy/jbcsrc/ExpressionCompiler.java | 6 + .../template/soy/jbcsrc/ProtoUtils.java | 50 +++++++ .../jssrc/internal/NullSafeAccumulator.java | 8 ++ .../internal/TranslateExprNodeVisitor.java | 42 +++++- .../soy/passes/ResolveDottedImportsPass.java | 32 +++-- .../internal/TranslateToPyExprVisitor.java | 1 + .../soy/shared/internal/BuiltinMethod.java | 84 +++++++++++ .../soy/sharedpasses/render/EvalVisitor.java | 3 + .../soy/types/ProtoOneofCaseImportType.java | 45 ++++++ .../soy/types/SoyProtoOneofCaseEnumType.java | 133 ++++++++++++++++++ .../template/soy/types/TypeInterner.java | 5 + .../template/soy/types/TypeRegistries.java | 13 ++ .../template/soy/jssrc/internal/JspbTest.java | 18 +++ 16 files changed, 485 insertions(+), 19 deletions(-) create mode 100644 java/src/com/google/template/soy/types/ProtoOneofCaseImportType.java create mode 100644 java/src/com/google/template/soy/types/SoyProtoOneofCaseEnumType.java diff --git a/java/src/com/google/template/soy/data/SoyProtoValue.java b/java/src/com/google/template/soy/data/SoyProtoValue.java index 180500c148..922e278c43 100644 --- a/java/src/com/google/template/soy/data/SoyProtoValue.java +++ b/java/src/com/google/template/soy/data/SoyProtoValue.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.base.CaseFormat; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -27,8 +28,10 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; +import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.protobuf.Message; import com.google.protobuf.TextFormat; +import com.google.template.soy.data.restricted.IntegerData; import com.google.template.soy.data.restricted.UndefinedData; import com.google.template.soy.internal.proto.Field; import com.google.template.soy.internal.proto.Int64ConversionMode; @@ -60,12 +63,16 @@ public final class SoyProtoValue extends SoyRecord { private static final class ProtoClass { final ImmutableMap fields; + final ImmutableMap oneofs; final Message defaultInstance; final String topLevelName; final String fullName; final String importPath; - ProtoClass(Message defaultInstance, ImmutableMap fields) { + ProtoClass( + Message defaultInstance, + ImmutableMap fields, + ImmutableMap oneofs) { this.fullName = defaultInstance.getDescriptorForType().getFullName(); Descriptor d = defaultInstance.getDescriptorForType(); while (d.getContainingType() != null) { @@ -75,6 +82,7 @@ private static final class ProtoClass { this.importPath = defaultInstance.getDescriptorForType().getFile().getFullName(); this.defaultInstance = checkNotNull(defaultInstance); this.fields = checkNotNull(fields); + this.oneofs = checkNotNull(oneofs); } } @@ -137,9 +145,19 @@ public void assignField(Message.Builder builder, SoyValue value) { @Override public ProtoClass load(Descriptor descriptor) throws Exception { Set extensions = new LinkedHashSet<>(); + ImmutableMap.Builder oneofs = ImmutableMap.builder(); + descriptor + .getOneofs() + .forEach( + o -> + oneofs.put( + CaseFormat.LOWER_UNDERSCORE.to( + CaseFormat.LOWER_CAMEL, o.getName()), + o)); return new ProtoClass( getDefaultInstance(descriptor), - Field.getFieldsForType(descriptor, extensions, factory)); + Field.getFieldsForType(descriptor, extensions, factory), + oneofs.build()); } }); @@ -262,6 +280,16 @@ public boolean hasProtoField(String name) { } } + public SoyValue getProtoOneofCase(String name) { + OneofDescriptor oneof = clazz().oneofs.get(name); + if (oneof == null) { + throw new IllegalArgumentException( + "Proto " + proto.getClass().getName() + " does not have a oneof of name " + name); + } + FieldDescriptor setField = proto.getOneofFieldDescriptor(oneof); + return IntegerData.forValue(setField != null ? setField.getNumber() : 0); + } + public void setAccessLocationKey(Object location) { this.locationKey = location; } diff --git a/java/src/com/google/template/soy/exprtree/ProtoEnumValueNode.java b/java/src/com/google/template/soy/exprtree/ProtoEnumValueNode.java index 87cf2cf9cb..e4f748e824 100644 --- a/java/src/com/google/template/soy/exprtree/ProtoEnumValueNode.java +++ b/java/src/com/google/template/soy/exprtree/ProtoEnumValueNode.java @@ -20,15 +20,17 @@ import com.google.template.soy.base.internal.Identifier; import com.google.template.soy.basetree.CopyState; import com.google.template.soy.types.SoyProtoEnumType; +import com.google.template.soy.types.SoyType; +import javax.annotation.Nullable; /** Node representing a proto enum value. */ public final class ProtoEnumValueNode extends AbstractPrimitiveNode { private final Identifier id; - private final SoyProtoEnumType type; + private final SoyType type; private final int enumNumber; - public ProtoEnumValueNode(Identifier id, SoyProtoEnumType type, int enumNumber) { + public ProtoEnumValueNode(Identifier id, SoyType type, int enumNumber) { super(id.location()); this.id = id; this.type = type; @@ -52,7 +54,7 @@ public Kind getKind() { } @Override - public SoyProtoEnumType getType() { + public SoyType getType() { return type; } @@ -65,8 +67,12 @@ public int getValueAsInt() { return enumNumber; } + @Nullable public EnumValueDescriptor getEnumValueDescriptor() { - return type.getDescriptor().findValueByNumber(enumNumber); + if (type instanceof SoyProtoEnumType soyProtoEnumType) { + return soyProtoEnumType.getDescriptor().findValueByNumber(enumNumber); + } + return null; } @Override diff --git a/java/src/com/google/template/soy/idomsrc/IdomTranslateExprNodeVisitor.java b/java/src/com/google/template/soy/idomsrc/IdomTranslateExprNodeVisitor.java index 6d26a25c31..8e778a6188 100644 --- a/java/src/com/google/template/soy/idomsrc/IdomTranslateExprNodeVisitor.java +++ b/java/src/com/google/template/soy/idomsrc/IdomTranslateExprNodeVisitor.java @@ -51,6 +51,8 @@ import com.google.template.soy.logging.LoggingFunction; import com.google.template.soy.shared.internal.BuiltinFunction; import com.google.template.soy.soytree.defn.TemplateStateVar; +import com.google.template.soy.types.SoyProtoEnumType; +import com.google.template.soy.types.SoyProtoOneofCaseEnumType; import com.google.template.soy.types.SoyType; import com.google.template.soy.types.SoyType.Kind; import com.google.template.soy.types.SoyTypes; @@ -161,8 +163,18 @@ protected JsType jsTypeFor(SoyType type) { @Override protected Expression visitProtoEnumValueNode(ProtoEnumValueNode node) { - return GoogRequire.create(node.getType().getNameForBackend(SoyBackendKind.JS_SRC)) - .googModuleGet() - .dotAccess(Ascii.toUpperCase(node.getEnumValueDescriptor().getName())); + String module; + String valueName; + if (node.getType() instanceof SoyProtoEnumType enumType) { + module = enumType.getNameForBackend(SoyBackendKind.JS_SRC); + valueName = node.getEnumValueDescriptor().getName(); + } else if (node.getType() instanceof SoyProtoOneofCaseEnumType oneofCaseType) { + module = oneofCaseType.getNameForBackend(SoyBackendKind.JS_SRC); + valueName = oneofCaseType.getNameForValue((int) node.getValue()); + } else { + throw new AssertionError("Unexpected type: " + node.getType()); + } + + return GoogRequire.create(module).googModuleGet().dotAccess(Ascii.toUpperCase(valueName)); } } diff --git a/java/src/com/google/template/soy/jbcsrc/ExpressionCompiler.java b/java/src/com/google/template/soy/jbcsrc/ExpressionCompiler.java index 0451f9eaba..692ea35101 100644 --- a/java/src/com/google/template/soy/jbcsrc/ExpressionCompiler.java +++ b/java/src/com/google/template/soy/jbcsrc/ExpressionCompiler.java @@ -1723,6 +1723,12 @@ private SoyExpression visitMethodCall(SoyExpression baseExpr, MethodCallNode nod ProtoUtils.SingularFieldAccessMode.DEFAULT_IF_UNSET, varManager, Int64ConversionMode.FOLLOW_JS_TYPE); + case GET_PROTO_ONEOF_CASE: + return ProtoUtils.accessOneofCase( + baseExpr, + BuiltinMethod.getProtoFieldNameFromMethodCall(node), + node.getType(), + varManager); case GET_PROTO_FIELD: return ProtoUtils.accessField( baseExpr, diff --git a/java/src/com/google/template/soy/jbcsrc/ProtoUtils.java b/java/src/com/google/template/soy/jbcsrc/ProtoUtils.java index c6e2c6eaad..178f29134b 100644 --- a/java/src/com/google/template/soy/jbcsrc/ProtoUtils.java +++ b/java/src/com/google/template/soy/jbcsrc/ProtoUtils.java @@ -33,6 +33,7 @@ import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.newLabel; import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.numericConversion; +import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -257,6 +258,55 @@ static SoyExpression accessField( } } + static SoyExpression accessOneofCase( + SoyExpression baseExpr, + String oneofName, + SoyType fieldType, + LocalVariableManager varManager) { + SoyType type = baseExpr.soyType().getEffectiveType(); + if (type instanceof SoyProtoType soyProtoType) { + return accessOneofCase(soyProtoType, baseExpr, oneofName); + } else { + return accessProtoUnionField( + baseExpr, + oneofName, + fieldType, + varManager, + expr -> { + SoyProtoType protoType = expr.soyType().asType(SoyProtoType.class); + return accessOneofCase(protoType, expr, oneofName); + }); + } + } + + private static SoyExpression accessOneofCase( + SoyProtoType protoType, SoyExpression baseExpr, String oneofName) { + OneofDescriptor descriptor = + protoType.getDescriptor().getOneofs().stream() + .filter( + o -> + CaseFormat.LOWER_UNDERSCORE + .to(CaseFormat.LOWER_CAMEL, o.getName()) + .equals(oneofName)) + .findFirst() + .get(); + SoyRuntimeType unboxedRuntimeType = SoyRuntimeType.getUnboxedType(protoType).get(); + Expression typedBaseExpr = baseExpr.unboxAsMessageUnchecked(unboxedRuntimeType.runtimeType()); + MethodRef getCaseMethod = getOneOfCaseMethod(descriptor); + Expression invokeGetCase = typedBaseExpr.invoke(getCaseMethod); + + MethodRef getNumberMethod = + MethodRef.createInstanceMethod( + TypeInfo.createClass(JavaQualifiedNames.getCaseEnumClassName(descriptor)), + new Method("getNumber", Type.INT_TYPE, MethodRef.NO_METHOD_ARGS), + MethodPureness.PURE); + + Expression getNumber = invokeGetCase.invoke(getNumberMethod); + Expression longNumber = numericConversion(getNumber, Type.LONG_TYPE); + + return SoyExpression.forInt(longNumber); + } + /** * Returns a {@link SoyExpression} for accessing a field of a proto, limited to a single type of * proto if the baseExpr can be a union of protos. diff --git a/java/src/com/google/template/soy/jssrc/internal/NullSafeAccumulator.java b/java/src/com/google/template/soy/jssrc/internal/NullSafeAccumulator.java index 7e2d6d65cb..1ac9b9a6d5 100644 --- a/java/src/com/google/template/soy/jssrc/internal/NullSafeAccumulator.java +++ b/java/src/com/google/template/soy/jssrc/internal/NullSafeAccumulator.java @@ -31,6 +31,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.ForOverride; import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.template.soy.internal.proto.ProtoUtils; import com.google.template.soy.jssrc.dsl.CodeChunk; import com.google.template.soy.jssrc.dsl.Expression; @@ -478,6 +479,7 @@ private enum Type { GET("get", ""), GET_OR_UNDEFINED("get", "OrUndefined"), GET_READONLY("getReadonly", ""), + GET_ONEOF_CASE("get", "Case"), HAS("has", ""); private final String prefix; @@ -538,6 +540,12 @@ static ProtoCall hasField(String fieldName, FieldDescriptor desc) { return accessor(fieldName, desc, Type.HAS); } + static ProtoCall getOneofCase(String oneofName, OneofDescriptor desc) { + String getter = "get" + LOWER_CAMEL.to(UPPER_CAMEL, oneofName) + "Case"; + return new AutoValue_NullSafeAccumulator_ProtoCall( + getter, /* getterArg= */ null, /* accessType= */ null, /* unpackFunction= */ null); + } + private static ProtoCall accessor(String fieldName, FieldDescriptor desc, Type type) { String getter; Expression arg; diff --git a/java/src/com/google/template/soy/jssrc/internal/TranslateExprNodeVisitor.java b/java/src/com/google/template/soy/jssrc/internal/TranslateExprNodeVisitor.java index 6f19717b44..cc1ea2f15d 100644 --- a/java/src/com/google/template/soy/jssrc/internal/TranslateExprNodeVisitor.java +++ b/java/src/com/google/template/soy/jssrc/internal/TranslateExprNodeVisitor.java @@ -68,6 +68,7 @@ import static com.google.template.soy.passes.ContentSecurityPolicyNonceInjectionPass.CSP_STYLE_NONCE_VARIABLE_NAME; import com.google.common.base.Ascii; +import com.google.common.base.CaseFormat; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -76,6 +77,7 @@ import com.google.common.collect.Iterables; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.base.SoyBackendKind; import com.google.template.soy.base.internal.Identifier; @@ -153,6 +155,8 @@ import com.google.template.soy.types.ListType; import com.google.template.soy.types.MapType; import com.google.template.soy.types.NumberType; +import com.google.template.soy.types.SoyProtoEnumType; +import com.google.template.soy.types.SoyProtoOneofCaseEnumType; import com.google.template.soy.types.SoyProtoType; import com.google.template.soy.types.SoyType; import com.google.template.soy.types.SoyTypes; @@ -344,10 +348,23 @@ protected Expression visitStringNode(StringNode node) { @Override protected Expression visitProtoEnumValueNode(ProtoEnumValueNode node) { - String module = node.getType().getNameForBackend(SoyBackendKind.JS_SRC); + String module; + String valueName; + if (node.getType() instanceof SoyProtoEnumType) { + SoyProtoEnumType enumType = (SoyProtoEnumType) node.getType(); + module = enumType.getNameForBackend(SoyBackendKind.JS_SRC); + valueName = node.getEnumValueDescriptor().getName(); + } else if (node.getType() instanceof SoyProtoOneofCaseEnumType) { + SoyProtoOneofCaseEnumType oneofCaseType = (SoyProtoOneofCaseEnumType) node.getType(); + module = oneofCaseType.getNameForBackend(SoyBackendKind.JS_SRC); + valueName = oneofCaseType.getNameForValue((int) node.getValue()); + } else { + throw new AssertionError("Unexpected type: " + node.getType()); + } + return GoogRequire.create(module) .reference() - .dotAccess(Id.create(Ascii.toUpperCase(node.getEnumValueDescriptor().getName()))); + .dotAccess(Id.create(Ascii.toUpperCase(valueName))); } // ----------------------------------------------------------------------------------------------- @@ -789,6 +806,27 @@ private NullSafeAccumulator genCodeForMethodCall( ProtoCall.getReadonlyField( fieldName, ((SoyProtoType) type).getFieldDescriptor(fieldName))); return base.dotAccess(fieldAccess, nullSafe); + case GET_PROTO_ONEOF_CASE: + String oneofName = BuiltinMethod.getProtoFieldNameFromMethodCall(methodCallNode); + fieldAccess = + genCodeForMaybeUnion( + baseType, + oneofName, + sourceLocation, + type -> { + SoyProtoType protoType = (SoyProtoType) type; + OneofDescriptor oneof = + protoType.getDescriptor().getOneofs().stream() + .filter( + o -> + CaseFormat.LOWER_UNDERSCORE + .to(CaseFormat.LOWER_CAMEL, o.getName()) + .equals(oneofName)) + .findFirst() + .get(); + return ProtoCall.getOneofCase(oneofName, oneof); + }); + return base.dotAccess(fieldAccess, nullSafe); // When adding new built-in methods it may be necessary to assert that the base expression // is not null in order to prevent a method call on a null instance from ever succeeding. case MAP_GET: diff --git a/java/src/com/google/template/soy/passes/ResolveDottedImportsPass.java b/java/src/com/google/template/soy/passes/ResolveDottedImportsPass.java index c55061d007..a841eba59a 100644 --- a/java/src/com/google/template/soy/passes/ResolveDottedImportsPass.java +++ b/java/src/com/google/template/soy/passes/ResolveDottedImportsPass.java @@ -20,6 +20,7 @@ import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.base.internal.IdGenerator; import com.google.template.soy.base.internal.Identifier; @@ -48,7 +49,9 @@ import com.google.template.soy.types.NamespaceType; import com.google.template.soy.types.ProtoEnumImportType; import com.google.template.soy.types.ProtoImportType; +import com.google.template.soy.types.ProtoOneofCaseImportType; import com.google.template.soy.types.SoyProtoEnumType; +import com.google.template.soy.types.SoyProtoOneofCaseEnumType; import com.google.template.soy.types.SoyType; import com.google.template.soy.types.StringType; import com.google.template.soy.types.TemplateImportType; @@ -175,15 +178,28 @@ private ExprNode resolveField( SoyType type = defn.type(); if (defn.getSymbolKind() == SymbolKind.PROTO_ENUM) { - EnumDescriptor enumDescriptor = ((ProtoEnumImportType) type).getDescriptor(); - EnumValueDescriptor val = enumDescriptor.findValueByName(fieldName); Identifier id = Identifier.create(refn.getName() + "." + fieldName, fullLocation); - SoyProtoEnumType soyType = typeRegistry.getOrCreateProtoEnumType(enumDescriptor); - if (val != null) { - return new ProtoEnumValueNode(id, soyType, val.getNumber()); - } else { - errorReporter.report(fullLocation, ENUM_MEMBERSHIP_ERROR, fieldName, refn.getName()); - return new ProtoEnumValueNode(id, soyType, 0); + if (type instanceof ProtoEnumImportType) { + EnumDescriptor enumDescriptor = ((ProtoEnumImportType) type).getDescriptor(); + EnumValueDescriptor val = enumDescriptor.findValueByName(fieldName); + SoyProtoEnumType soyType = typeRegistry.getOrCreateProtoEnumType(enumDescriptor); + if (val != null) { + return new ProtoEnumValueNode(id, soyType, val.getNumber()); + } else { + errorReporter.report(fullLocation, ENUM_MEMBERSHIP_ERROR, fieldName, refn.getName()); + return new ProtoEnumValueNode(id, soyType, 0); + } + } else if (type instanceof ProtoOneofCaseImportType protoOneofCaseImportType) { + OneofDescriptor oneofDescriptor = protoOneofCaseImportType.getDescriptor(); + SoyProtoOneofCaseEnumType soyType = + typeRegistry.getOrCreateProtoOneofCaseEnumType(oneofDescriptor); + Integer val = soyType.getValue(fieldName); + if (val != null) { + return new ProtoEnumValueNode(id, soyType, val); + } else { + errorReporter.report(fullLocation, ENUM_MEMBERSHIP_ERROR, fieldName, refn.getName()); + return new ProtoEnumValueNode(id, soyType, 0); + } } } diff --git a/java/src/com/google/template/soy/pysrc/internal/TranslateToPyExprVisitor.java b/java/src/com/google/template/soy/pysrc/internal/TranslateToPyExprVisitor.java index e44afe42cc..b345f9b57e 100644 --- a/java/src/com/google/template/soy/pysrc/internal/TranslateToPyExprVisitor.java +++ b/java/src/com/google/template/soy/pysrc/internal/TranslateToPyExprVisitor.java @@ -889,6 +889,7 @@ private String genCodeForMethodCall(MethodCallNode methodCallNode, PyExpr contai case GET_PROTO_FIELD: case GET_READONLY_PROTO_FIELD: case GET_PROTO_FIELD_OR_UNDEFINED: + case GET_PROTO_ONEOF_CASE: case FUNCTION_BIND: errorReporter.report( methodCallNode.getAccessSourceLocation(), diff --git a/java/src/com/google/template/soy/shared/internal/BuiltinMethod.java b/java/src/com/google/template/soy/shared/internal/BuiltinMethod.java index 2aadb310a8..68e357358d 100644 --- a/java/src/com/google/template/soy/shared/internal/BuiltinMethod.java +++ b/java/src/com/google/template/soy/shared/internal/BuiltinMethod.java @@ -31,6 +31,7 @@ import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.Descriptors.GenericDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.template.soy.compilermetrics.Impression; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.SoyErrorKind; @@ -315,6 +316,74 @@ ImmutableCollection expandMethodNames(SoyType baseType, List ar } }, + GET_PROTO_ONEOF_CASE("get[X]Case", 0) { + @Override + public boolean appliesToBase(SoyType baseType) { + Preconditions.checkArgument(!SoyTypes.isNullish(baseType)); + return MessageType.getInstance().isAssignableFromStrict(baseType); + } + + @Override + public boolean appliesTo(String methodName, SoyType baseType) { + var oneofName = getOneofName(methodName); + return oneofName.isPresent() && appliesToProto(oneofName.get(), baseType); + } + + private boolean appliesToProto(String oneofName, SoyType baseType) { + if (!appliesToBase(baseType)) { + return false; + } + for (SoyType type : SoyTypes.flattenUnionToSet(baseType)) { + SoyProtoType protoType = (SoyProtoType) type; + if (!hasOneof(protoType, oneofName)) { + return false; + } + } + return true; + } + + private boolean hasOneof(SoyProtoType protoType, String oneofName) { + return protoType.getDescriptor().getOneofs().stream() + .anyMatch(o -> toLowerCamel(o.getName()).equals(oneofName)); + } + + private String toLowerCamel(String snake) { + return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, snake); + } + + @Override + public SoyType getReturnType( + String methodName, + SoyType baseType, + List params, + SoyTypeRegistry soyTypeRegistry, + ErrorReporter errorReporter) { + String oneofName = getOneofName(methodName).get(); + SoyProtoType protoType = (SoyProtoType) SoyTypes.flattenUnion(baseType).iterator().next(); + OneofDescriptor oneof = + protoType.getDescriptor().getOneofs().stream() + .filter(o -> toLowerCamel(o.getName()).equals(oneofName)) + .findFirst() + .get(); + return soyTypeRegistry.getOrCreateProtoOneofCaseEnumType(oneof); + } + + @Override + ImmutableCollection expandMethodNames(SoyType baseType, List argTypes) { + if (!appliesToBase(baseType)) { + return ImmutableList.of(); + } + SoyProtoType protoType = (SoyProtoType) SoyTypes.flattenUnion(baseType).iterator().next(); + return protoType.getDescriptor().getOneofs().stream() + .map( + o -> + "get" + + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, o.getName()) + + "Case") + .collect(toImmutableSet()); + } + }, + /** Soy method that gets a single value of a map. */ MAP_GET("get", 1) { @@ -469,6 +538,7 @@ public ImmutableListMultimap matchForBaseAndArgs( BuiltinMethod.GET_PROTO_FIELD, BuiltinMethod.GET_PROTO_FIELD_OR_UNDEFINED, BuiltinMethod.GET_READONLY_PROTO_FIELD, + BuiltinMethod.GET_PROTO_ONEOF_CASE, BuiltinMethod.GET_EXTENSION, BuiltinMethod.GET_READONLY_EXTENSION); @@ -487,6 +557,8 @@ public static String getProtoFieldNameFromMethodCall(MethodCallNode node) { return getGetOrUndefinedFieldName(methodName).get(); case GET_READONLY_PROTO_FIELD: return getGetReadonlyFieldName(methodName).get(); + case GET_PROTO_ONEOF_CASE: + return getOneofName(methodName).get(); case GET_EXTENSION: case GET_READONLY_EXTENSION: case HAS_EXTENSION: @@ -645,6 +717,7 @@ public List getProtoDependencyTypes(MethodCallNode methodNode case GET_PROTO_FIELD: case GET_PROTO_FIELD_OR_UNDEFINED: case GET_READONLY_PROTO_FIELD: + case GET_PROTO_ONEOF_CASE: case MAP_GET: case FUNCTION_BIND: case BIND: @@ -732,6 +805,17 @@ private static Optional getGetOrUndefinedAsStringFieldName(String method return Optional.of(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, middle)); } + private static Optional getOneofName(String methodName) { + if (!methodName.startsWith("get") || !methodName.endsWith("Case")) { + return Optional.empty(); + } + String middle = methodName.substring(3, methodName.length() - "Case".length()); + if (middle.length() > 0 && !Ascii.isUpperCase(middle.charAt(0))) { + return Optional.empty(); + } + return Optional.of(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, middle)); + } + private static SoyType computeTypeForProtoFieldName( SoyType baseType, String fieldName, diff --git a/java/src/com/google/template/soy/sharedpasses/render/EvalVisitor.java b/java/src/com/google/template/soy/sharedpasses/render/EvalVisitor.java index 6f93d1f948..8128a979b3 100644 --- a/java/src/com/google/template/soy/sharedpasses/render/EvalVisitor.java +++ b/java/src/com/google/template/soy/sharedpasses/render/EvalVisitor.java @@ -729,6 +729,9 @@ private SoyValue visitMethodCallNode(MethodCallNode methodNode, SoyValue base) { case GET_READONLY_PROTO_FIELD: return ((SoyProtoValue) base) .getReadonlyProtoField(BuiltinMethod.getProtoFieldNameFromMethodCall(methodNode)); + case GET_PROTO_ONEOF_CASE: + return ((SoyProtoValue) base) + .getProtoOneofCase(BuiltinMethod.getProtoFieldNameFromMethodCall(methodNode)); case GET_PROTO_FIELD_OR_UNDEFINED: return ((SoyProtoValue) base) .getProtoFieldOrNull( diff --git a/java/src/com/google/template/soy/types/ProtoOneofCaseImportType.java b/java/src/com/google/template/soy/types/ProtoOneofCaseImportType.java new file mode 100644 index 0000000000..2c297eba02 --- /dev/null +++ b/java/src/com/google/template/soy/types/ProtoOneofCaseImportType.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Google Inc. + * + * 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 + * + * http://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 com.google.template.soy.types; + +import com.google.auto.value.AutoValue; +import com.google.common.base.CaseFormat; +import com.google.protobuf.Descriptors.OneofDescriptor; + +/** Representing an imported synthesized proto oneof case enum type. */ +@AutoValue +public abstract class ProtoOneofCaseImportType extends ImportType { + + public static ProtoOneofCaseImportType create(OneofDescriptor descriptor) { + return new AutoValue_ProtoOneofCaseImportType(descriptor); + } + + public abstract OneofDescriptor getDescriptor(); + + @Override + public final String toString() { + return getDescriptor().getContainingType().getFullName() + + "." + + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, getDescriptor().getName()) + + "Case"; + } + + @Override + public Kind getKind() { + return Kind.PROTO_ENUM_TYPE; + } +} diff --git a/java/src/com/google/template/soy/types/SoyProtoOneofCaseEnumType.java b/java/src/com/google/template/soy/types/SoyProtoOneofCaseEnumType.java new file mode 100644 index 0000000000..46df6f787c --- /dev/null +++ b/java/src/com/google/template/soy/types/SoyProtoOneofCaseEnumType.java @@ -0,0 +1,133 @@ +/* + * Copyright 2026 Google Inc. + * + * 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 + * + * http://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 com.google.template.soy.types; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.CaseFormat; +import com.google.common.base.Objects; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; +import com.google.template.soy.base.SoyBackendKind; +import com.google.template.soy.internal.proto.ProtoUtils; +import com.google.template.soy.soytree.SoyTypeP; + +/** A {@link SoyType} implementation which describes a synthesized proto oneof case enum type. */ +public final class SoyProtoOneofCaseEnumType extends SoyType { + + public static SoyProtoOneofCaseEnumType create(OneofDescriptor descriptor) { + return new SoyProtoOneofCaseEnumType(checkNotNull(descriptor)); + } + + private final OneofDescriptor descriptor; + + private SoyProtoOneofCaseEnumType(OneofDescriptor descriptor) { + this.descriptor = descriptor; + } + + @Override + public Kind getKind() { + return Kind.PROTO_ENUM; // We treat it as a proto enum in the type system + } + + @Override + boolean doIsAssignableFromNonUnionType(SoyType fromType) { + return fromType == this + || (fromType.getClass() == this.getClass() + && ((SoyProtoOneofCaseEnumType) fromType).descriptor == descriptor); + } + + public String getName() { + return descriptor.getContainingType().getFullName() + + "." + + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, descriptor.getName()) + + "Case"; + } + + public String getNameForBackend(SoyBackendKind backend) { + return switch (backend) { + case JS_SRC -> + ProtoUtils.calculateUnprefixedJsName(descriptor.getContainingType()) + + "." + + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, descriptor.getName()) + + "Case"; + case TOFU -> ProtoUtils.getQualifiedOuterClassname(descriptor); + case PYTHON_SRC, JBC_SRC -> throw new UnsupportedOperationException(); + }; + } + + public Integer getValue(String memberName) { + String upperOneofName = + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, descriptor.getName()); + if (memberName.equals(upperOneofName + "_NOT_SET")) { + return 0; + } + for (FieldDescriptor field : descriptor.getFields()) { + String upperFieldName = + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, field.getName()); + if (memberName.equals(upperFieldName)) { + return field.getNumber(); + } + } + return null; + } + + public String getNameForValue(int value) { + String upperOneofName = + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, descriptor.getName()); + if (value == 0) { + return upperOneofName + "_NOT_SET"; + } + for (FieldDescriptor field : descriptor.getFields()) { + if (field.getNumber() == value) { + return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, field.getName()); + } + } + return null; + } + + public OneofDescriptor getDescriptor() { + return descriptor; + } + + @Override + public String toString() { + return getName(); + } + + @Override + protected void doToProto(SoyTypeP.Builder builder) { + builder.setProtoEnum(getName()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SoyProtoOneofCaseEnumType that = (SoyProtoOneofCaseEnumType) o; + return Objects.equal(descriptor.getFullName(), that.descriptor.getFullName()); + } + + @Override + public int hashCode() { + return java.util.Objects.hashCode(descriptor.getFullName()); + } +} diff --git a/java/src/com/google/template/soy/types/TypeInterner.java b/java/src/com/google/template/soy/types/TypeInterner.java index 7925b543c0..23824433af 100644 --- a/java/src/com/google/template/soy/types/TypeInterner.java +++ b/java/src/com/google/template/soy/types/TypeInterner.java @@ -21,6 +21,7 @@ import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.Descriptors.GenericDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.template.soy.data.restricted.PrimitiveData; import com.google.template.soy.types.SanitizedType.ElementType; import java.util.Arrays; @@ -173,6 +174,10 @@ default SoyProtoEnumType getOrCreateProtoEnumType(EnumDescriptor descriptor) { return intern(SoyProtoEnumType.create(descriptor)); } + default SoyProtoOneofCaseEnumType getOrCreateProtoOneofCaseEnumType(OneofDescriptor descriptor) { + return intern(SoyProtoOneofCaseEnumType.create(descriptor)); + } + default SoyType getOrCreateElementType(String tagName) { return intern(ElementType.getInstance(tagName)); } diff --git a/java/src/com/google/template/soy/types/TypeRegistries.java b/java/src/com/google/template/soy/types/TypeRegistries.java index e915277a2f..9a46b44845 100644 --- a/java/src/com/google/template/soy/types/TypeRegistries.java +++ b/java/src/com/google/template/soy/types/TypeRegistries.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.template.soy.types.SoyTypes.INT_OR_FLOAT; +import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; @@ -29,6 +30,7 @@ import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.Descriptors.GenericDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.template.soy.base.internal.Identifier; import com.google.template.soy.compilermetrics.Impression; import com.google.template.soy.error.ErrorReporter; @@ -131,6 +133,9 @@ public ImportType getProtoImportType(GenericDescriptor descriptor) { if (d instanceof EnumDescriptor) { return ProtoEnumImportType.create((EnumDescriptor) d); } + if (d instanceof OneofDescriptor oneofDescriptor) { + return ProtoOneofCaseImportType.create(oneofDescriptor); + } if (d instanceof FieldDescriptor && ((FieldDescriptor) d).isExtension()) { return ProtoExtensionImportType.create((FieldDescriptor) d); } @@ -172,6 +177,14 @@ private static ImmutableMap buildMemberIndex(GenericD descriptor.getNestedTypes().forEach(t -> index.put(t.getName(), t)); descriptor.getEnumTypes().forEach(t -> index.put(t.getName(), t)); descriptor.getExtensions().forEach(t -> index.put(Field.computeSoyName(t), t)); + descriptor + .getOneofs() + .forEach( + t -> + index.put( + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, t.getName()) + + "Case", + t)); } else { throw new AssertionError(d.getClass().getName()); } diff --git a/java/tests/com/google/template/soy/jssrc/internal/JspbTest.java b/java/tests/com/google/template/soy/jssrc/internal/JspbTest.java index 3d4e0ebe25..249bee43a7 100644 --- a/java/tests/com/google/template/soy/jssrc/internal/JspbTest.java +++ b/java/tests/com/google/template/soy/jssrc/internal/JspbTest.java @@ -117,6 +117,24 @@ public void testProto3Fields_oneof() { .withPrecedence(TIMES); } + @Test + public void testProtoOneofCase() { + assertThatSoyExpr(expr("$msg.getTheseCase()").withParam("{@param msg: Proto3Message}")) + .withProtoImports(DESCRIPTORS) + .generatesCode("opt_data.msg.getTheseCase();"); + } + + @Test + public void testProtoOneofCaseComparison() { + assertThatSoyExpr( + expr("$msg.getTheseCase() == Proto3Message.TheseCase.ANOTHER_INT_FIELD") + .withParam("{@param msg: Proto3Message}")) + .withProtoImports(DESCRIPTORS) + .generatesCode( + "opt_data.msg.getTheseCase() ==" + + " proto.soy.test3.Proto3Message.TheseCase.ANOTHER_INT_FIELD;"); + } + @Test public void testGetExtension() { assertThatSoyExpr(