diff --git a/reflect/protodesc/editions.go b/reflect/protodesc/editions.go index bf0a0ccd..f55b0369 100644 --- a/reflect/protodesc/editions.go +++ b/reflect/protodesc/editions.go @@ -125,16 +125,27 @@ func mergeEditionFeatures(parentDesc protoreflect.Descriptor, child *descriptorp parentFS.IsJSONCompliant = *jf == descriptorpb.FeatureSet_ALLOW } - if goFeatures, ok := proto.GetExtension(child, gofeaturespb.E_Go).(*gofeaturespb.GoFeatures); ok && goFeatures != nil { - if luje := goFeatures.LegacyUnmarshalJsonEnum; luje != nil { - parentFS.GenerateLegacyUnmarshalJSON = *luje - } - if sep := goFeatures.StripEnumPrefix; sep != nil { - parentFS.StripEnumPrefix = int(*sep) - } - if al := goFeatures.ApiLevel; al != nil { - parentFS.APILevel = int(*al) - } + // We must not use proto.GetExtension(child, gofeaturespb.E_Go) + // because that only works for messages we generated, but not for + // dynamicpb messages. See golang/protobuf#1669. + goFeatures := child.ProtoReflect().Get(gofeaturespb.E_Go.TypeDescriptor()) + if !goFeatures.IsValid() { + return parentFS + } + // gf.Interface() could be *dynamicpb.Message or *gofeaturespb.GoFeatures. + gf := goFeatures.Message() + fields := gf.Descriptor().Fields() + + if fd := fields.ByName("legacy_unmarshal_json_enum"); gf.Has(fd) { + parentFS.GenerateLegacyUnmarshalJSON = gf.Get(fd).Bool() + } + + if fd := fields.ByName("strip_enum_prefix"); gf.Has(fd) { + parentFS.StripEnumPrefix = int(gf.Get(fd).Enum()) + } + + if fd := fields.ByName("api_level"); gf.Has(fd) { + parentFS.APILevel = int(gf.Get(fd).Enum()) } return parentFS diff --git a/reflect/protodesc/editions_test.go b/reflect/protodesc/editions_test.go new file mode 100644 index 00000000..91c5f388 --- /dev/null +++ b/reflect/protodesc/editions_test.go @@ -0,0 +1,47 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protodesc + +import ( + "testing" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/dynamicpb" + "google.golang.org/protobuf/types/gofeaturespb" +) + +func TestGoFeaturesDynamic(t *testing.T) { + md := (*gofeaturespb.GoFeatures)(nil).ProtoReflect().Descriptor() + gf := dynamicpb.NewMessage(md) + opaque := protoreflect.ValueOfEnum(gofeaturespb.GoFeatures_API_OPAQUE.Number()) + gf.Set(md.Fields().ByName("api_level"), opaque) + featureSet := &descriptorpb.FeatureSet{} + dynamicExt := dynamicpb.NewExtensionType(gofeaturespb.E_Go.TypeDescriptor().Descriptor()) + proto.SetExtension(featureSet, dynamicExt, gf) + + fd := &descriptorpb.FileDescriptorProto{ + Name: proto.String("a.proto"), + Dependency: []string{ + "google/protobuf/go_features.proto", + }, + Edition: descriptorpb.Edition_EDITION_2023.Enum(), + Syntax: proto.String("editions"), + Options: &descriptorpb.FileOptions{ + Features: featureSet, + }, + } + fds := &descriptorpb.FileDescriptorSet{ + File: []*descriptorpb.FileDescriptorProto{ + ToFileDescriptorProto(descriptorpb.File_google_protobuf_descriptor_proto), + ToFileDescriptorProto(gofeaturespb.File_google_protobuf_go_features_proto), + fd, + }, + } + if _, err := NewFiles(fds); err != nil { + t.Fatal(err) + } +}