[protobuf] compiler/protogen: introduce and expose feature resoluton to protogen

6 views
Skip to first unread message

Mike Kruskal (Gerrit)

unread,
Dec 12, 2025, 12:14:51 AM (6 days ago) Dec 12
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Mike Kruskal has uploaded the change for review

Commit message

compiler/protogen: introduce and expose feature resoluton to protogen

This will allow go plugins to specify any custom extensions they care about using protoc to generate a FeatureSetDefaults object. They can then access all ResolvedFeatures of any descriptors to do more involved codegen for languages outside of go.
Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8

Change diff

diff --git a/cmd/protoc-gen-go/feature_resolution_test/feature_resolution_test.go b/cmd/protoc-gen-go/feature_resolution_test/feature_resolution_test.go
new file mode 100644
index 0000000..ec7e9b1
--- /dev/null
+++ b/cmd/protoc-gen-go/feature_resolution_test/feature_resolution_test.go
@@ -0,0 +1,809 @@
+// Copyright 2018 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 feature_resolution_test
+
+import (
+ _ "embed"
+ "fmt"
+ "slices"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ basicpb "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/featureresolution"
+ testfeaturespb "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/features"
+ "google.golang.org/protobuf/compiler/protogen"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protodesc"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/runtime/protoimpl"
+ descpb "google.golang.org/protobuf/types/descriptorpb"
+ "google.golang.org/protobuf/types/gofeaturespb"
+ "google.golang.org/protobuf/types/pluginpb"
+)
+
+var (
+ //go:embed test_features_defaults.binpb
+ featureSetDefaultsRaw []byte
+ featureSetDefaults *descpb.FeatureSetDefaults
+)
+
+func init() {
+ featureSetDefaults = &descpb.FeatureSetDefaults{}
+ if err := proto.Unmarshal(featureSetDefaultsRaw, featureSetDefaults); err != nil {
+ panic(err)
+ }
+}
+
+func createTestFile() *descpb.FileDescriptorProto {
+ return protodesc.ToFileDescriptorProto(basicpb.File_cmd_protoc_gen_go_testdata_featureresolution_basic_proto)
+}
+
+func protogenFor(t *testing.T, f *descpb.FileDescriptorProto) *protogen.File {
+ t.Helper()
+
+ // Construct a Protobuf plugin code generation request based on the
+ // transitive closure of dependencies of message m.
+ req := &pluginpb.CodeGeneratorRequest{
+ ProtoFile: []*descpb.FileDescriptorProto{
+ protodesc.ToFileDescriptorProto(descpb.File_google_protobuf_descriptor_proto),
+ protodesc.ToFileDescriptorProto(gofeaturespb.File_google_protobuf_go_features_proto),
+ protodesc.ToFileDescriptorProto(testfeaturespb.File_cmd_protoc_gen_go_testdata_features_test_features_proto),
+ f,
+ },
+ }
+ plugin, err := protogen.Options{
+ FeatureSetDefaults: featureSetDefaults,
+ }.New(req)
+ if err != nil {
+ t.Fatalf("protogen.Options.New: %v", err)
+ }
+ if got, want := len(plugin.Files), len(req.ProtoFile); got != want {
+ t.Fatalf("protogen returned %d plugin.Files entries, expected %d", got, want)
+ }
+ // The last file topologically is the one that we care about.
+ return plugin.Files[len(plugin.Files)-1]
+}
+
+func checkFeature(t *testing.T, features *descpb.FeatureSet, ext *protoimpl.ExtensionInfo, name string, value any) {
+ t.Helper()
+ reflect := features.ProtoReflect()
+ if ext != nil {
+ reflect = proto.GetExtension(features, ext).(protoreflect.ProtoMessage).ProtoReflect()
+ }
+ field := reflect.Descriptor().Fields().ByName(protoreflect.Name(name))
+ if field == nil {
+ t.Fatalf("feature %q not found", name)
+ }
+ got := reflect.Get(field)
+ want := protoreflect.ValueOf(value)
+ if eq := cmp.Equal(got, want); !eq {
+ t.Errorf("feature %q = %v, want %v", name, got, want)
+ }
+}
+
+func checkGlobalFeature(t *testing.T, features *descpb.FeatureSet, name string, value any) {
+ t.Helper()
+ checkFeature(t, features, nil, name, value)
+}
+
+func checkTestFeature(t *testing.T, features *descpb.FeatureSet, name string, value any) {
+ t.Helper()
+ checkFeature(t, features, testfeaturespb.E_TestFeatures, name, value)
+}
+
+type testCase struct {
+ name string
+ features *descpb.FeatureSet
+}
+
+func createAllTestCases(file *protogen.File) []testCase {
+ testCases := []testCase{
+ {
+ name: "file",
+ features: file.ResolvedFeatures,
+ },
+ {
+ name: "top_message",
+ features: file.Messages[0].ResolvedFeatures,
+ },
+ {
+ name: "top_enum",
+ features: file.Enums[0].ResolvedFeatures,
+ },
+ {
+ name: "top_enum_value",
+ features: file.Enums[0].Values[0].ResolvedFeatures,
+ },
+ {
+ name: "field",
+ features: file.Messages[0].Fields[0].ResolvedFeatures,
+ },
+ {
+ name: "oneof",
+ features: file.Messages[0].Oneofs[0].ResolvedFeatures,
+ },
+ {
+ name: "oneof_field",
+ features: file.Messages[0].Fields[1].ResolvedFeatures,
+ },
+ {
+ name: "nested_message",
+ features: file.Messages[0].Messages[0].ResolvedFeatures,
+ },
+ {
+ name: "nested_field",
+ features: file.Messages[0].Messages[0].Fields[0].ResolvedFeatures,
+ },
+ {
+ name: "nested_enum",
+ features: file.Messages[0].Enums[0].ResolvedFeatures,
+ },
+ {
+ name: "nested_enum_value",
+ features: file.Messages[0].Enums[0].Values[0].ResolvedFeatures,
+ },
+ {
+ name: "service",
+ features: file.Services[0].ResolvedFeatures,
+ },
+ {
+ name: "method",
+ features: file.Services[0].Methods[0].ResolvedFeatures,
+ },
+ }
+ if file.Proto.GetSyntax() != "proto3" {
+ // Extensions aren't allowed in proto3.
+ testCases = append(testCases,
+ testCase{
+ name: "extension",
+ features: file.Extensions[0].ResolvedFeatures,
+ },
+ testCase{
+ name: "nested_extension",
+ features: file.Messages[0].Extensions[0].ResolvedFeatures,
+ },
+ )
+ }
+ return testCases
+}
+
+// splitTestCases splits the provided test cases into two sets: the first set
+// contains all test cases that are covered by the provided names and the
+// second set contains all other test cases.
+func splitTestCases(testCases []testCase, split []string) ([]testCase, []testCase) {
+ var covered []testCase
+ var uncovered []testCase
+ reached := make(map[string]bool)
+ for _, tc := range testCases {
+ if slices.Contains(split, tc.name) {
+ covered = append(covered, tc)
+ reached[tc.name] = true
+ } else {
+ uncovered = append(uncovered, tc)
+ }
+ }
+ if len(reached) != len(split) {
+ panic(fmt.Sprintf("%d test cases not found", len(split)-len(reached)))
+ }
+ return covered, uncovered
+}
+
+func TestProto2Defaults(t *testing.T) {
+ fd := createTestFile()
+ fd.Syntax = proto.String("proto2")
+ fd.Edition = nil
+ file := protogenFor(t, fd)
+
+ for _, tc := range createAllTestCases(file) {
+ t.Run(tc.name, func(t *testing.T) {
+ checkGlobalFeature(t, tc.features, "field_presence", descpb.FeatureSet_EXPLICIT.Number())
+ checkTestFeature(t, tc.features, "enum_feature", testfeaturespb.EnumFeature_VALUE1.Number())
+ checkTestFeature(t, tc.features, "bool_feature", false)
+ })
+ }
+}
+
+func TestProto3Defaults(t *testing.T) {
+ fd := createTestFile()
+ fd.Syntax = proto.String("proto3")
+ fd.Edition = nil
+ fd.MessageType[0].ExtensionRange = nil
+ fd.MessageType[0].Extension = nil
+ fd.Extension = nil
+ file := protogenFor(t, fd)
+
+ for _, tc := range createAllTestCases(file) {
+ t.Run(tc.name, func(t *testing.T) {
+ checkGlobalFeature(t, tc.features, "field_presence", descpb.FeatureSet_IMPLICIT.Number())
+ checkTestFeature(t, tc.features, "enum_feature", testfeaturespb.EnumFeature_VALUE1.Number())
+ checkTestFeature(t, tc.features, "bool_feature", false)
+ })
+ }
+}
+
+func TestEdition2023Defaults(t *testing.T) {
+ fd := createTestFile()
+ file := protogenFor(t, fd)
+
+ for _, tc := range createAllTestCases(file) {
+ t.Run(tc.name, func(t *testing.T) {
+ checkGlobalFeature(t, tc.features, "field_presence", descpb.FeatureSet_EXPLICIT.Number())
+ checkTestFeature(t, tc.features, "enum_feature", testfeaturespb.EnumFeature_VALUE2.Number())
+ checkTestFeature(t, tc.features, "bool_feature", true)
+ })
+ }
+}
+
+func TestEditionUnstable(t *testing.T) {
+ fd := createTestFile()
+ fd.Edition = descpb.Edition_EDITION_UNSTABLE.Enum()
+ file := protogenFor(t, fd)
+
+ // Note: this should be VALUE2 but 33.2 doesn't produce the correct defaults for EDITION_UNSTABLE.
+ checkTestFeature(t, file.ResolvedFeatures, "unstable_feature", testfeaturespb.EnumFeature_VALUE1.Number())
+}
+
+func TestInheritance(t *testing.T) {
+ tests := []struct {
+ name string
+ setup func(*descpb.FileDescriptorProto, *descpb.FeatureSet)
+ inherit []string
+ }{
+ {
+ name: "file",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.Options.Features = f
+ },
+ inherit: []string{
+ "file",
+ "top_message",
+ "top_enum",
+ "top_enum_value",
+ "field",
+ "oneof",
+ "oneof_field",
+ "nested_message",
+ "nested_enum",
+ "nested_enum_value",
+ "nested_extension",
+ "nested_field",
+ "extension",
+ "service",
+ "method",
+ },
+ },
+ {
+ name: "message",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].Options = &descpb.MessageOptions{Features: f}
+ },
+ inherit: []string{
+ "top_message",
+ "field",
+ "oneof",
+ "oneof_field",
+ "nested_message",
+ "nested_enum",
+ "nested_enum_value",
+ "nested_extension",
+ "nested_field",
+ },
+ },
+ {
+ name: "field",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].Field[0].Options = &descpb.FieldOptions{Features: f}
+ },
+ inherit: []string{
+ "field",
+ },
+ },
+ {
+ name: "oneof",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].OneofDecl[0].Options = &descpb.OneofOptions{Features: f}
+ },
+ inherit: []string{
+ "oneof",
+ "oneof_field",
+ },
+ },
+ {
+ name: "oneof_field",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].Field[1].Options = &descpb.FieldOptions{Features: f}
+ },
+ inherit: []string{
+ "oneof_field",
+ },
+ },
+ {
+ name: "nested_message",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].NestedType[0].Options = &descpb.MessageOptions{Features: f}
+ },
+ inherit: []string{
+ "nested_message",
+ "nested_field",
+ },
+ },
+ {
+ name: "nested_field",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].NestedType[0].Field[0].Options = &descpb.FieldOptions{Features: f}
+ },
+ inherit: []string{
+ "nested_field",
+ },
+ },
+ {
+ name: "nested_extension",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].Extension[0].Options = &descpb.FieldOptions{Features: f}
+ },
+ inherit: []string{
+ "nested_extension",
+ },
+ },
+ {
+ name: "nested_enum",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].EnumType[0].Options = &descpb.EnumOptions{Features: f}
+ },
+ inherit: []string{
+ "nested_enum",
+ "nested_enum_value",
+ },
+ },
+ {
+ name: "nested_enum_value",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.MessageType[0].EnumType[0].Value[0].Options = &descpb.EnumValueOptions{Features: f}
+ },
+ inherit: []string{
+ "nested_enum_value",
+ },
+ },
+ {
+ name: "enum",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.EnumType[0].Options = &descpb.EnumOptions{Features: f}
+ },
+ inherit: []string{
+ "top_enum",
+ "top_enum_value",
+ },
+ },
+ {
+ name: "enum_value",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.EnumType[0].Value[0].Options = &descpb.EnumValueOptions{Features: f}
+ },
+ inherit: []string{
+ "top_enum_value",
+ },
+ },
+ {
+ name: "extension",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.Extension[0].Options = &descpb.FieldOptions{Features: f}
+ },
+ inherit: []string{
+ "extension",
+ },
+ },
+ {
+ name: "service",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.Service[0].Options = &descpb.ServiceOptions{Features: f}
+ },
+ inherit: []string{
+ "service",
+ "method",
+ },
+ },
+ {
+ name: "method",
+ setup: func(fd *descpb.FileDescriptorProto, f *descpb.FeatureSet) {
+ fd.Service[0].Method[0].Options = &descpb.MethodOptions{Features: f}
+ },
+ inherit: []string{
+ "method",
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ fd := createTestFile()
+
+ features := &descpb.FeatureSet{
+ FieldPresence: descpb.FeatureSet_IMPLICIT.Enum(),
+ }
+ proto.SetExtension(features, testfeaturespb.E_TestFeatures, testfeaturespb.TestFeatures_builder{
+ EnumFeature: testfeaturespb.EnumFeature_VALUE4.Enum(),
+ }.Build())
+
+ tc.setup(fd, features)
+ file := protogenFor(t, fd)
+
+ inherit, def := splitTestCases(createAllTestCases(file), tc.inherit)
+
+ for _, tc2 := range inherit {
+ t.Run(tc2.name, func(t *testing.T) {
+ checkGlobalFeature(t, tc2.features, "field_presence", descpb.FeatureSet_IMPLICIT.Number())
+ checkTestFeature(t, tc2.features, "enum_feature", testfeaturespb.EnumFeature_VALUE4.Number())
+ })
+ }
+
+ for _, tc2 := range def {
+ t.Run(tc2.name, func(t *testing.T) {
+ checkGlobalFeature(t, tc2.features, "field_presence", descpb.FeatureSet_EXPLICIT.Number())
+ checkTestFeature(t, tc2.features, "enum_feature", testfeaturespb.EnumFeature_VALUE2.Number())
+ })
+ }
+ })
+ }
+}
+
+func TestOverride(t *testing.T) {
+ tests := []struct {
+ name string
+ setup func(*descpb.FileDescriptorProto, *descpb.FeatureSet, *descpb.FeatureSet)
+ inherit []string
+ override []string
+ }{
+ {
+ name: "file_message",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.Options.Features = f1
+ fd.MessageType[0].Options = &descpb.MessageOptions{Features: f2}
+ },
+ inherit: []string{
+ "file",
+ "top_enum",
+ "top_enum_value",
+ "extension",
+ "service",
+ "method",
+ },
+ override: []string{
+ "top_message",
+ "field",
+ "oneof",
+ "oneof_field",
+ "nested_message",
+ "nested_enum",
+ "nested_enum_value",
+ "nested_extension",
+ "nested_field",
+ },
+ },
+ {
+ name: "file_enum",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.Options.Features = f1
+ fd.EnumType[0].Options = &descpb.EnumOptions{Features: f2}
+ },
+ inherit: []string{
+ "file",
+ "top_message",
+ "field",
+ "oneof",
+ "oneof_field",
+ "nested_message",
+ "nested_enum",
+ "nested_enum_value",
+ "nested_extension",
+ "nested_field",
+ "extension",
+ "service",
+ "method",
+ },
+ override: []string{
+ "top_enum",
+ "top_enum_value",
+ },
+ },
+ {
+ name: "message_enum",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.MessageType[0].Options = &descpb.MessageOptions{Features: f1}
+ fd.MessageType[0].EnumType[0].Options = &descpb.EnumOptions{Features: f2}
+ },
+ inherit: []string{
+ "top_message",
+ "field",
+ "oneof",
+ "oneof_field",
+ "nested_message",
+ "nested_extension",
+ "nested_field",
+ },
+ override: []string{
+ "nested_enum",
+ "nested_enum_value",
+ },
+ },
+ {
+ name: "message_message",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.MessageType[0].Options = &descpb.MessageOptions{Features: f1}
+ fd.MessageType[0].NestedType[0].Options = &descpb.MessageOptions{Features: f2}
+ },
+ inherit: []string{
+ "top_message",
+ "field",
+ "oneof",
+ "oneof_field",
+ "nested_enum",
+ "nested_enum_value",
+ "nested_extension",
+ },
+ override: []string{
+ "nested_message",
+ "nested_field",
+ },
+ },
+ {
+ name: "message_oneof",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.MessageType[0].Options = &descpb.MessageOptions{Features: f1}
+ fd.MessageType[0].OneofDecl[0].Options = &descpb.OneofOptions{Features: f2}
+ },
+ inherit: []string{
+ "top_message",
+ "field",
+ "nested_message",
+ "nested_enum",
+ "nested_enum_value",
+ "nested_extension",
+ "nested_field",
+ },
+ override: []string{
+ "oneof",
+ "oneof_field",
+ },
+ },
+ {
+ name: "message_extension",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.MessageType[0].Options = &descpb.MessageOptions{Features: f1}
+ fd.MessageType[0].Extension[0].Options = &descpb.FieldOptions{Features: f2}
+ },
+ inherit: []string{
+ "top_message",
+ "field",
+ "nested_message",
+ "nested_field",
+ "nested_enum",
+ "nested_enum_value",
+ "oneof",
+ "oneof_field",
+ },
+ override: []string{
+ "nested_extension",
+ },
+ },
+ {
+ name: "message_field",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.MessageType[0].Options = &descpb.MessageOptions{Features: f1}
+ fd.MessageType[0].Field[0].Options = &descpb.FieldOptions{Features: f2}
+ },
+ inherit: []string{
+ "top_message",
+ "nested_message",
+ "nested_field",
+ "nested_extension",
+ "nested_enum",
+ "nested_enum_value",
+ "oneof",
+ "oneof_field",
+ },
+ override: []string{
+ "field",
+ },
+ },
+ {
+ name: "enum_value",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.EnumType[0].Options = &descpb.EnumOptions{Features: f1}
+ fd.EnumType[0].Value[0].Options = &descpb.EnumValueOptions{Features: f2}
+ },
+ inherit: []string{
+ "top_enum",
+ },
+ override: []string{
+ "top_enum_value",
+ },
+ },
+ {
+ name: "file_extension",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.Options.Features = f1
+ fd.Extension[0].Options = &descpb.FieldOptions{Features: f2}
+ },
+ inherit: []string{
+ "file",
+ "top_message",
+ "top_enum",
+ "top_enum_value",
+ "field",
+ "oneof",
+ "oneof_field",
+ "nested_message",
+ "nested_enum",
+ "nested_enum_value",
+ "nested_extension",
+ "nested_field",
+ "service",
+ "method",
+ },
+ override: []string{
+ "extension",
+ },
+ },
+ {
+ name: "file_service",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.Options.Features = f1
+ fd.Service[0].Options = &descpb.ServiceOptions{Features: f2}
+ },
+ inherit: []string{
+ "file",
+ "top_message",
+ "top_enum",
+ "top_enum_value",
+ "field",
+ "oneof",
+ "oneof_field",
+ "nested_message",
+ "nested_enum",
+ "nested_enum_value",
+ "nested_extension",
+ "nested_field",
+ "extension",
+ },
+ override: []string{
+ "service",
+ "method",
+ },
+ },
+ {
+ name: "service_method",
+ setup: func(fd *descpb.FileDescriptorProto, f1 *descpb.FeatureSet, f2 *descpb.FeatureSet) {
+ fd.Service[0].Options = &descpb.ServiceOptions{Features: f1}
+ fd.Service[0].Method[0].Options = &descpb.MethodOptions{Features: f2}
+ },
+ inherit: []string{
+ "service",
+ },
+ override: []string{
+ "method",
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ fd := createTestFile()
+
+ features1 := &descpb.FeatureSet{
+ FieldPresence: descpb.FeatureSet_IMPLICIT.Enum(),
+ }
+ proto.SetExtension(features1, testfeaturespb.E_TestFeatures, testfeaturespb.TestFeatures_builder{
+ EnumFeature: testfeaturespb.EnumFeature_VALUE4.Enum(),
+ }.Build())
+
+ features2 := &descpb.FeatureSet{
+ FieldPresence: descpb.FeatureSet_EXPLICIT.Enum(),
+ }
+ proto.SetExtension(features2, testfeaturespb.E_TestFeatures, testfeaturespb.TestFeatures_builder{
+ EnumFeature: testfeaturespb.EnumFeature_VALUE5.Enum(),
+ }.Build())
+
+ tc.setup(fd, features1, features2)
+ file := protogenFor(t, fd)
+
+ changed, def := splitTestCases(createAllTestCases(file), append(tc.inherit, tc.override...))
+ override, inherit := splitTestCases(changed, tc.override)
+
+ for _, tc2 := range inherit {
+ t.Run(tc2.name, func(t *testing.T) {
+ checkGlobalFeature(t, tc2.features, "field_presence", descpb.FeatureSet_IMPLICIT.Number())
+ checkTestFeature(t, tc2.features, "enum_feature", testfeaturespb.EnumFeature_VALUE4.Number())
+ })
+ }
+
+ for _, tc2 := range override {
+ t.Run(tc2.name, func(t *testing.T) {
+ checkGlobalFeature(t, tc2.features, "field_presence", descpb.FeatureSet_EXPLICIT.Number())
+ checkTestFeature(t, tc2.features, "enum_feature", testfeaturespb.EnumFeature_VALUE5.Number())
+ })
+ }
+
+ for _, tc2 := range def {
+ t.Run(tc2.name, func(t *testing.T) {
+ checkGlobalFeature(t, tc2.features, "field_presence", descpb.FeatureSet_EXPLICIT.Number())
+ checkTestFeature(t, tc2.features, "enum_feature", testfeaturespb.EnumFeature_VALUE2.Number())
+ })
+ }
+ })
+ }
+}
+
+func TestErrorEditionTooEarly(t *testing.T) {
+ fd := createTestFile()
+ req := &pluginpb.CodeGeneratorRequest{
+ ProtoFile: []*descpb.FileDescriptorProto{
+ fd,
+ },
+ }
+
+ defaults := proto.Clone(featureSetDefaults).(*descpb.FeatureSetDefaults)
+ defaults.MinimumEdition = descpb.Edition_EDITION_2024.Enum()
+ _, err := protogen.Options{
+ FeatureSetDefaults: defaults,
+ }.New(req)
+
+ if err == nil {
+ t.Error("protogen.Options.New: got nil, want error")
+ }
+
+ if want := "lower than the minimum supported edition"; !strings.Contains(err.Error(), want) {
+ t.Errorf("protogen.Options.New: got error %v, want error containing %q", err, want)
+ }
+}
+
+func TestErrorEditionTooLate(t *testing.T) {
+ fd := createTestFile()
+ fd.Edition = descpb.Edition_EDITION_2024.Enum()
+ req := &pluginpb.CodeGeneratorRequest{
+ ProtoFile: []*descpb.FileDescriptorProto{
+ fd,
+ },
+ }
+
+ defaults := proto.Clone(featureSetDefaults).(*descpb.FeatureSetDefaults)
+ defaults.MaximumEdition = descpb.Edition_EDITION_2023.Enum()
+ _, err := protogen.Options{
+ FeatureSetDefaults: defaults,
+ }.New(req)
+
+ if err == nil {
+ t.Error("protogen.Options.New: got nil, want error")
+ }
+
+ if !strings.Contains(err.Error(), "greater than the maximum supported edition") {
+ t.Errorf("protogen.Options.New: got error %v, want error containing %q", err, "greater than the maximum supported edition")
+ }
+}
+
+func TestErrorInvalidDefaults(t *testing.T) {
+ fd := createTestFile()
+ fd.Syntax = proto.String("proto2")
+ fd.Edition = nil
+ req := &pluginpb.CodeGeneratorRequest{
+ ProtoFile: []*descpb.FileDescriptorProto{
+ fd,
+ },
+ }
+
+ defaults := proto.Clone(featureSetDefaults).(*descpb.FeatureSetDefaults)
+ defaults.Defaults = defaults.GetDefaults()[2:]
+ _, err := protogen.Options{
+ FeatureSetDefaults: defaults,
+ }.New(req)
+
+ if err == nil {
+ t.Error("protogen.Options.New: got nil, want error")
+ }
+
+ if !strings.Contains(err.Error(), "does not have a default") {
+ t.Errorf("protogen.Options.New: got error %v, want error containing %q", err, "does not have a default")
+ }
+}
diff --git a/cmd/protoc-gen-go/feature_resolution_test/test_features_defaults.binpb b/cmd/protoc-gen-go/feature_resolution_test/test_features_defaults.binpb
new file mode 100644
index 0000000..9d921a3
--- /dev/null
+++ b/cmd/protoc-gen-go/feature_resolution_test/test_features_defaults.binpb
Binary files differ
diff --git a/cmd/protoc-gen-go/testdata/featureresolution/basic.pb.go b/cmd/protoc-gen-go/testdata/featureresolution/basic.pb.go
new file mode 100644
index 0000000..9ee76a2
--- /dev/null
+++ b/cmd/protoc-gen-go/testdata/featureresolution/basic.pb.go
@@ -0,0 +1,354 @@
+// 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.
+
+// This file contains a basic file layout for testing feature resolution. It is
+// constructed in a way where it can be easily transformed to newer editions.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: cmd/protoc-gen-go/testdata/featureresolution/basic.proto
+
+package featureresolution
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+type TopEnum int32
+
+const (
+ TopEnum_TOP_ENUM_UNKNOWN TopEnum = 0
+ TopEnum_TOP_ENUM_VALUE1 TopEnum = 1
+ TopEnum_TOP_ENUM_VALUE2 TopEnum = 2
+)
+
+// Enum value maps for TopEnum.
+var (
+ TopEnum_name = map[int32]string{
+ 0: "TOP_ENUM_UNKNOWN",
+ 1: "TOP_ENUM_VALUE1",
+ 2: "TOP_ENUM_VALUE2",
+ }
+ TopEnum_value = map[string]int32{
+ "TOP_ENUM_UNKNOWN": 0,
+ "TOP_ENUM_VALUE1": 1,
+ "TOP_ENUM_VALUE2": 2,
+ }
+)
+
+func (x TopEnum) Enum() *TopEnum {
+ p := new(TopEnum)
+ *p = x
+ return p
+}
+
+func (x TopEnum) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (TopEnum) Descriptor() protoreflect.EnumDescriptor {
+ return file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_enumTypes[0].Descriptor()
+}
+
+func (TopEnum) Type() protoreflect.EnumType {
+ return &file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_enumTypes[0]
+}
+
+func (x TopEnum) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use TopEnum.Descriptor instead.
+func (TopEnum) EnumDescriptor() ([]byte, []int) {
+ return file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescGZIP(), []int{0}
+}
+
+type TopMessage_NestedEnum int32
+
+const (
+ TopMessage_UNKNOWN TopMessage_NestedEnum = 0
+ TopMessage_VALUE1 TopMessage_NestedEnum = 1
+ TopMessage_VALUE2 TopMessage_NestedEnum = 2
+)
+
+// Enum value maps for TopMessage_NestedEnum.
+var (
+ TopMessage_NestedEnum_name = map[int32]string{
+ 0: "UNKNOWN",
+ 1: "VALUE1",
+ 2: "VALUE2",
+ }
+ TopMessage_NestedEnum_value = map[string]int32{
+ "UNKNOWN": 0,
+ "VALUE1": 1,
+ "VALUE2": 2,
+ }
+)
+
+func (x TopMessage_NestedEnum) Enum() *TopMessage_NestedEnum {
+ p := new(TopMessage_NestedEnum)
+ *p = x
+ return p
+}
+
+func (x TopMessage_NestedEnum) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (TopMessage_NestedEnum) Descriptor() protoreflect.EnumDescriptor {
+ return file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_enumTypes[1].Descriptor()
+}
+
+func (TopMessage_NestedEnum) Type() protoreflect.EnumType {
+ return &file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_enumTypes[1]
+}
+
+func (x TopMessage_NestedEnum) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use TopMessage_NestedEnum.Descriptor instead.
+func (TopMessage_NestedEnum) EnumDescriptor() ([]byte, []int) {
+ return file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescGZIP(), []int{0, 0}
+}
+
+type TopMessage struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Field *int32 `protobuf:"varint,1,opt,name=field" json:"field,omitempty"`
+ // Types that are valid to be assigned to O:
+ //
+ // *TopMessage_OneofField
+ O isTopMessage_O `protobuf_oneof:"o"`
+ extensionFields protoimpl.ExtensionFields
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TopMessage) Reset() {
+ *x = TopMessage{}
+ mi := &file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TopMessage) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TopMessage) ProtoMessage() {}
+
+func (x *TopMessage) ProtoReflect() protoreflect.Message {
+ mi := &file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use TopMessage.ProtoReflect.Descriptor instead.
+func (*TopMessage) Descriptor() ([]byte, []int) {
+ return file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *TopMessage) GetField() int32 {
+ if x != nil && x.Field != nil {
+ return *x.Field
+ }
+ return 0
+}
+
+func (x *TopMessage) GetO() isTopMessage_O {
+ if x != nil {
+ return x.O
+ }
+ return nil
+}
+
+func (x *TopMessage) GetOneofField() string {
+ if x != nil {
+ if x, ok := x.O.(*TopMessage_OneofField); ok {
+ return x.OneofField
+ }
+ }
+ return ""
+}
+
+type isTopMessage_O interface {
+ isTopMessage_O()
+}
+
+type TopMessage_OneofField struct {
+ OneofField string `protobuf:"bytes,2,opt,name=oneof_field,json=oneofField,oneof"`
+}
+
+func (*TopMessage_OneofField) isTopMessage_O() {}
+
+type TopMessage_NestedMessage struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ NumericField *int32 `protobuf:"varint,1,opt,name=numeric_field,json=numericField" json:"numeric_field,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TopMessage_NestedMessage) Reset() {
+ *x = TopMessage_NestedMessage{}
+ mi := &file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TopMessage_NestedMessage) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TopMessage_NestedMessage) ProtoMessage() {}
+
+func (x *TopMessage_NestedMessage) ProtoReflect() protoreflect.Message {
+ mi := &file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use TopMessage_NestedMessage.ProtoReflect.Descriptor instead.
+func (*TopMessage_NestedMessage) Descriptor() ([]byte, []int) {
+ return file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescGZIP(), []int{0, 0}
+}
+
+func (x *TopMessage_NestedMessage) GetNumericField() int32 {
+ if x != nil && x.NumericField != nil {
+ return *x.NumericField
+ }
+ return 0
+}
+
+var file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_extTypes = []protoimpl.ExtensionInfo{
+ {
+ ExtendedType: (*TopMessage)(nil),
+ ExtensionType: (*string)(nil),
+ Field: 104,
+ Name: "net.proto2.go.testdata.featureresolution.ext",
+ Tag: "bytes,104,opt,name=ext",
+ Filename: "cmd/protoc-gen-go/testdata/featureresolution/basic.proto",
+ },
+ {
+ ExtendedType: (*TopMessage)(nil),
+ ExtensionType: (*int32)(nil),
+ Field: 100,
+ Name: "net.proto2.go.testdata.featureresolution.TopMessage.ext",
+ Tag: "varint,100,opt,name=ext",
+ Filename: "cmd/protoc-gen-go/testdata/featureresolution/basic.proto",
+ },
+}
+
+// Extension fields to TopMessage.
+var (
+ // optional string ext = 104;
+ E_Ext = &file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_extTypes[0]
+ // optional int32 ext = 100;
+ E_TopMessage_Ext = &file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_extTypes[1]
+)
+
+var File_cmd_protoc_gen_go_testdata_featureresolution_basic_proto protoreflect.FileDescriptor
+
+const file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDesc = "" +
+ "\n" +
+ "8cmd/protoc-gen-go/testdata/featureresolution/basic.proto\x12(net.proto2.go.testdata.featureresolution\"\x82\x02\n" +
+ "\n" +
+ "TopMessage\x12\x14\n" +
+ "\x05field\x18\x01 \x01(\x05R\x05field\x12!\n" +
+ "\voneof_field\x18\x02 \x01(\tH\x00R\n" +
+ "oneofField\x1a4\n" +
+ "\rNestedMessage\x12#\n" +
+ "\rnumeric_field\x18\x01 \x01(\x05R\fnumericField\"1\n" +
+ "\n" +
+ "NestedEnum\x12\v\n" +
+ "\aUNKNOWN\x10\x00\x12\n" +
+ "\n" +
+ "\x06VALUE1\x10\x01\x12\n" +
+ "\n" +
+ "\x06VALUE2\x10\x02*\x05\bd\x10\xc8\x012F\n" +
+ "\x03ext\x124.net.proto2.go.testdata.featureresolution.TopMessage\x18d \x01(\x05R\x03extB\x03\n" +
+ "\x01o*I\n" +
+ "\aTopEnum\x12\x14\n" +
+ "\x10TOP_ENUM_UNKNOWN\x10\x00\x12\x13\n" +
+ "\x0fTOP_ENUM_VALUE1\x10\x01\x12\x13\n" +
+ "\x0fTOP_ENUM_VALUE2\x10\x022\x84\x01\n" +
+ "\n" +
+ "TopService\x12v\n" +
+ "\x06Method\x124.net.proto2.go.testdata.featureresolution.TopMessage\x1a4.net.proto2.go.testdata.featureresolution.TopMessage\"\x00:F\n" +
+ "\x03ext\x124.net.proto2.go.testdata.featureresolution.TopMessage\x18h \x01(\tR\x03extBIZGgoogle.golang.org/protobuf/cmd/protoc-gen-go/testdata/featureresolutionb\beditionsp\xe8\a"
+
+var (
+ file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescOnce sync.Once
+ file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescData []byte
+)
+
+func file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescGZIP() []byte {
+ file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescOnce.Do(func() {
+ file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDesc), len(file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDesc)))
+ })
+ return file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDescData
+}
+
+var file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
+var file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_goTypes = []any{
+ (TopEnum)(0), // 0: net.proto2.go.testdata.featureresolution.TopEnum
+ (TopMessage_NestedEnum)(0), // 1: net.proto2.go.testdata.featureresolution.TopMessage.NestedEnum
+ (*TopMessage)(nil), // 2: net.proto2.go.testdata.featureresolution.TopMessage
+ (*TopMessage_NestedMessage)(nil), // 3: net.proto2.go.testdata.featureresolution.TopMessage.NestedMessage
+}
+var file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_depIdxs = []int32{
+ 2, // 0: net.proto2.go.testdata.featureresolution.ext:extendee -> net.proto2.go.testdata.featureresolution.TopMessage
+ 2, // 1: net.proto2.go.testdata.featureresolution.TopMessage.ext:extendee -> net.proto2.go.testdata.featureresolution.TopMessage
+ 2, // 2: net.proto2.go.testdata.featureresolution.TopService.Method:input_type -> net.proto2.go.testdata.featureresolution.TopMessage
+ 2, // 3: net.proto2.go.testdata.featureresolution.TopService.Method:output_type -> net.proto2.go.testdata.featureresolution.TopMessage
+ 3, // [3:4] is the sub-list for method output_type
+ 2, // [2:3] is the sub-list for method input_type
+ 2, // [2:2] is the sub-list for extension type_name
+ 0, // [0:2] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_init() }
+func file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_init() {
+ if File_cmd_protoc_gen_go_testdata_featureresolution_basic_proto != nil {
+ return
+ }
+ file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_msgTypes[0].OneofWrappers = []any{
+ (*TopMessage_OneofField)(nil),
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDesc), len(file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_rawDesc)),
+ NumEnums: 2,
+ NumMessages: 2,
+ NumExtensions: 2,
+ NumServices: 1,
+ },
+ GoTypes: file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_goTypes,
+ DependencyIndexes: file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_depIdxs,
+ EnumInfos: file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_enumTypes,
+ MessageInfos: file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_msgTypes,
+ ExtensionInfos: file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_extTypes,
+ }.Build()
+ File_cmd_protoc_gen_go_testdata_featureresolution_basic_proto = out.File
+ file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_goTypes = nil
+ file_cmd_protoc_gen_go_testdata_featureresolution_basic_proto_depIdxs = nil
+}
diff --git a/cmd/protoc-gen-go/testdata/featureresolution/basic.proto b/cmd/protoc-gen-go/testdata/featureresolution/basic.proto
new file mode 100644
index 0000000..e6bd82c
--- /dev/null
+++ b/cmd/protoc-gen-go/testdata/featureresolution/basic.proto
@@ -0,0 +1,51 @@
+// 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.
+
+// This file contains a basic file layout for testing feature resolution. It is
+// constructed in a way where it can be easily transformed to newer editions.
+
+edition = "2023";
+
+package net.proto2.go.testdata.featureresolution;
+
+option go_package = "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/featureresolution";
+
+message TopMessage {
+ int32 field = 1;
+
+ oneof o {
+ string oneof_field = 2;
+ }
+
+ message NestedMessage {
+ int32 numeric_field = 1;
+ }
+
+ enum NestedEnum {
+ UNKNOWN = 0;
+ VALUE1 = 1;
+ VALUE2 = 2;
+ }
+
+ extensions 100 to 199;
+
+ extend TopMessage {
+ int32 ext = 100;
+ }
+}
+
+enum TopEnum {
+ TOP_ENUM_UNKNOWN = 0;
+ TOP_ENUM_VALUE1 = 1;
+ TOP_ENUM_VALUE2 = 2;
+}
+
+extend TopMessage {
+ string ext = 104;
+}
+
+service TopService {
+ // Test method.
+ rpc Method(TopMessage) returns (TopMessage) {}
+}
diff --git a/cmd/protoc-gen-go/testdata/features/test_features.pb.go b/cmd/protoc-gen-go/testdata/features/test_features.pb.go
new file mode 100644
index 0000000..09f2620
--- /dev/null
+++ b/cmd/protoc-gen-go/testdata/features/test_features.pb.go
@@ -0,0 +1,399 @@
+// 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.
+
+// This file contains a set of custom features for testing feature resolution of
+// arbitrary languages.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: cmd/protoc-gen-go/testdata/features/test_features.proto
+
+package features
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ descriptorpb "google.golang.org/protobuf/types/descriptorpb"
+ reflect "reflect"
+ unsafe "unsafe"
+)
+
+type EnumFeature int32
+
+const (
+ EnumFeature_TEST_ENUM_FEATURE_UNKNOWN EnumFeature = 0
+ EnumFeature_VALUE1 EnumFeature = 1
+ EnumFeature_VALUE2 EnumFeature = 2
+ EnumFeature_VALUE3 EnumFeature = 3
+ EnumFeature_VALUE4 EnumFeature = 4
+ EnumFeature_VALUE5 EnumFeature = 5
+)
+
+// Enum value maps for EnumFeature.
+var (
+ EnumFeature_name = map[int32]string{
+ 0: "TEST_ENUM_FEATURE_UNKNOWN",
+ 1: "VALUE1",
+ 2: "VALUE2",
+ 3: "VALUE3",
+ 4: "VALUE4",
+ 5: "VALUE5",
+ }
+ EnumFeature_value = map[string]int32{
+ "TEST_ENUM_FEATURE_UNKNOWN": 0,
+ "VALUE1": 1,
+ "VALUE2": 2,
+ "VALUE3": 3,
+ "VALUE4": 4,
+ "VALUE5": 5,
+ }
+)
+
+func (x EnumFeature) Enum() *EnumFeature {
+ p := new(EnumFeature)
+ *p = x
+ return p
+}
+
+func (x EnumFeature) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (EnumFeature) Descriptor() protoreflect.EnumDescriptor {
+ return file_cmd_protoc_gen_go_testdata_features_test_features_proto_enumTypes[0].Descriptor()
+}
+
+func (EnumFeature) Type() protoreflect.EnumType {
+ return &file_cmd_protoc_gen_go_testdata_features_test_features_proto_enumTypes[0]
+}
+
+func (x EnumFeature) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+type TestFeatures struct {
+ state protoimpl.MessageState `protogen:"opaque.v1"`
+ xxx_hidden_EnumFeature EnumFeature `protobuf:"varint,1,opt,name=enum_feature,json=enumFeature,enum=goproto.protoc.features.EnumFeature"`
+ xxx_hidden_BoolFeature bool `protobuf:"varint,2,opt,name=bool_feature,json=boolFeature"`
+ xxx_hidden_SourceFeature EnumFeature `protobuf:"varint,3,opt,name=source_feature,json=sourceFeature,enum=goproto.protoc.features.EnumFeature"`
+ xxx_hidden_FixedFeature EnumFeature `protobuf:"varint,4,opt,name=fixed_feature,json=fixedFeature,enum=goproto.protoc.features.EnumFeature"`
+ xxx_hidden_FutureFeature EnumFeature `protobuf:"varint,5,opt,name=future_feature,json=futureFeature,enum=goproto.protoc.features.EnumFeature"`
+ xxx_hidden_UnstableFeature EnumFeature `protobuf:"varint,7,opt,name=unstable_feature,json=unstableFeature,enum=goproto.protoc.features.EnumFeature"`
+ XXX_raceDetectHookData protoimpl.RaceDetectHookData
+ XXX_presence [1]uint32
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TestFeatures) Reset() {
+ *x = TestFeatures{}
+ mi := &file_cmd_protoc_gen_go_testdata_features_test_features_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TestFeatures) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TestFeatures) ProtoMessage() {}
+
+func (x *TestFeatures) ProtoReflect() protoreflect.Message {
+ mi := &file_cmd_protoc_gen_go_testdata_features_test_features_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+func (x *TestFeatures) GetEnumFeature() EnumFeature {
+ if x != nil {
+ if protoimpl.X.Present(&(x.XXX_presence[0]), 0) {
+ return x.xxx_hidden_EnumFeature
+ }
+ }
+ return EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) GetBoolFeature() bool {
+ if x != nil {
+ return x.xxx_hidden_BoolFeature
+ }
+ return false
+}
+
+func (x *TestFeatures) GetSourceFeature() EnumFeature {
+ if x != nil {
+ if protoimpl.X.Present(&(x.XXX_presence[0]), 2) {
+ return x.xxx_hidden_SourceFeature
+ }
+ }
+ return EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) GetFixedFeature() EnumFeature {
+ if x != nil {
+ if protoimpl.X.Present(&(x.XXX_presence[0]), 3) {
+ return x.xxx_hidden_FixedFeature
+ }
+ }
+ return EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) GetFutureFeature() EnumFeature {
+ if x != nil {
+ if protoimpl.X.Present(&(x.XXX_presence[0]), 4) {
+ return x.xxx_hidden_FutureFeature
+ }
+ }
+ return EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) GetUnstableFeature() EnumFeature {
+ if x != nil {
+ if protoimpl.X.Present(&(x.XXX_presence[0]), 5) {
+ return x.xxx_hidden_UnstableFeature
+ }
+ }
+ return EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) SetEnumFeature(v EnumFeature) {
+ x.xxx_hidden_EnumFeature = v
+ protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 6)
+}
+
+func (x *TestFeatures) SetBoolFeature(v bool) {
+ x.xxx_hidden_BoolFeature = v
+ protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 6)
+}
+
+func (x *TestFeatures) SetSourceFeature(v EnumFeature) {
+ x.xxx_hidden_SourceFeature = v
+ protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 6)
+}
+
+func (x *TestFeatures) SetFixedFeature(v EnumFeature) {
+ x.xxx_hidden_FixedFeature = v
+ protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 6)
+}
+
+func (x *TestFeatures) SetFutureFeature(v EnumFeature) {
+ x.xxx_hidden_FutureFeature = v
+ protoimpl.X.SetPresent(&(x.XXX_presence[0]), 4, 6)
+}
+
+func (x *TestFeatures) SetUnstableFeature(v EnumFeature) {
+ x.xxx_hidden_UnstableFeature = v
+ protoimpl.X.SetPresent(&(x.XXX_presence[0]), 5, 6)
+}
+
+func (x *TestFeatures) HasEnumFeature() bool {
+ if x == nil {
+ return false
+ }
+ return protoimpl.X.Present(&(x.XXX_presence[0]), 0)
+}
+
+func (x *TestFeatures) HasBoolFeature() bool {
+ if x == nil {
+ return false
+ }
+ return protoimpl.X.Present(&(x.XXX_presence[0]), 1)
+}
+
+func (x *TestFeatures) HasSourceFeature() bool {
+ if x == nil {
+ return false
+ }
+ return protoimpl.X.Present(&(x.XXX_presence[0]), 2)
+}
+
+func (x *TestFeatures) HasFixedFeature() bool {
+ if x == nil {
+ return false
+ }
+ return protoimpl.X.Present(&(x.XXX_presence[0]), 3)
+}
+
+func (x *TestFeatures) HasFutureFeature() bool {
+ if x == nil {
+ return false
+ }
+ return protoimpl.X.Present(&(x.XXX_presence[0]), 4)
+}
+
+func (x *TestFeatures) HasUnstableFeature() bool {
+ if x == nil {
+ return false
+ }
+ return protoimpl.X.Present(&(x.XXX_presence[0]), 5)
+}
+
+func (x *TestFeatures) ClearEnumFeature() {
+ protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0)
+ x.xxx_hidden_EnumFeature = EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) ClearBoolFeature() {
+ protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1)
+ x.xxx_hidden_BoolFeature = false
+}
+
+func (x *TestFeatures) ClearSourceFeature() {
+ protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 2)
+ x.xxx_hidden_SourceFeature = EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) ClearFixedFeature() {
+ protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 3)
+ x.xxx_hidden_FixedFeature = EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) ClearFutureFeature() {
+ protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 4)
+ x.xxx_hidden_FutureFeature = EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+func (x *TestFeatures) ClearUnstableFeature() {
+ protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 5)
+ x.xxx_hidden_UnstableFeature = EnumFeature_TEST_ENUM_FEATURE_UNKNOWN
+}
+
+type TestFeatures_builder struct {
+ _ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
+
+ EnumFeature *EnumFeature
+ BoolFeature *bool
+ SourceFeature *EnumFeature
+ FixedFeature *EnumFeature
+ FutureFeature *EnumFeature
+ UnstableFeature *EnumFeature
+}
+
+func (b0 TestFeatures_builder) Build() *TestFeatures {
+ m0 := &TestFeatures{}
+ b, x := &b0, m0
+ _, _ = b, x
+ if b.EnumFeature != nil {
+ protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 6)
+ x.xxx_hidden_EnumFeature = *b.EnumFeature
+ }
+ if b.BoolFeature != nil {
+ protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 6)
+ x.xxx_hidden_BoolFeature = *b.BoolFeature
+ }
+ if b.SourceFeature != nil {
+ protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 6)
+ x.xxx_hidden_SourceFeature = *b.SourceFeature
+ }
+ if b.FixedFeature != nil {
+ protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 6)
+ x.xxx_hidden_FixedFeature = *b.FixedFeature
+ }
+ if b.FutureFeature != nil {
+ protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 4, 6)
+ x.xxx_hidden_FutureFeature = *b.FutureFeature
+ }
+ if b.UnstableFeature != nil {
+ protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 5, 6)
+ x.xxx_hidden_UnstableFeature = *b.UnstableFeature
+ }
+ return m0
+}
+
+var file_cmd_protoc_gen_go_testdata_features_test_features_proto_extTypes = []protoimpl.ExtensionInfo{
+ {
+ ExtendedType: (*descriptorpb.FeatureSet)(nil),
+ ExtensionType: (*TestFeatures)(nil),
+ Field: 9999,
+ Name: "goproto.protoc.features.test_features",
+ Tag: "bytes,9999,opt,name=test_features",
+ Filename: "cmd/protoc-gen-go/testdata/features/test_features.proto",
+ },
+}
+
+// Extension fields to descriptorpb.FeatureSet.
+var (
+ // optional goproto.protoc.features.TestFeatures test_features = 9999;
+ E_TestFeatures = &file_cmd_protoc_gen_go_testdata_features_test_features_proto_extTypes[0]
+)
+
+var File_cmd_protoc_gen_go_testdata_features_test_features_proto protoreflect.FileDescriptor
+
+const file_cmd_protoc_gen_go_testdata_features_test_features_proto_rawDesc = "" +
+ "\n" +
+ "7cmd/protoc-gen-go/testdata/features/test_features.proto\x12\x17goproto.protoc.features\x1a google/protobuf/descriptor.proto\"\x96\x06\n" +
+ "\fTestFeatures\x12\x97\x01\n" +
+ "\fenum_feature\x18\x01 \x01(\x0e2$.goproto.protoc.features.EnumFeatureBN\x88\x01\x01\x98\x01\x01\x98\x01\x04\x98\x01\x03\x98\x01\x06\x98\x01\a\x98\x01\b\x98\x01\t\x98\x01\x05\x98\x01\x02\xa2\x01\v\x12\x06VALUE1\x18\x84\a\xa2\x01\v\x12\x06VALUE2\x18\xe8\a\xa2\x01\v\x12\x06VALUE3\x18\xe9\a\xb2\x01\x03\b\xe8\aR\venumFeature\x12`\n" +
+ "\fbool_feature\x18\x02 \x01(\bB=\x88\x01\x01\x98\x01\x01\x98\x01\x04\x98\x01\x03\x98\x01\x06\x98\x01\a\x98\x01\b\x98\x01\t\x98\x01\x05\x98\x01\x02\xa2\x01\n" +
+ "\x12\x05false\x18\x84\a\xa2\x01\t\x12\x04true\x18\xe8\a\xb2\x01\x03\b\xe8\aR\vboolFeature\x12\x7f\n" +
+ "\x0esource_feature\x18\x03 \x01(\x0e2$.goproto.protoc.features.EnumFeatureB2\x88\x01\x02\x98\x01\x01\x98\x01\x04\x98\x01\x03\x98\x01\x06\x98\x01\a\x98\x01\b\x98\x01\t\x98\x01\x05\x98\x01\x02\xa2\x01\v\x12\x06VALUE1\x18\x84\a\xb2\x01\x03\b\xe8\aR\rsourceFeature\x12y\n" +
+ "\rfixed_feature\x18\x04 \x01(\x0e2$.goproto.protoc.features.EnumFeatureB.\x88\x01\x01\x98\x01\x01\x98\x01\x04\xa2\x01\v\x12\x06VALUE1\x18\x84\a\xa2\x01\v\x12\x06VALUE2\x18\xe8\a\xb2\x01\x06\b\xe8\a \xe8\aR\ffixedFeature\x12x\n" +
+ "\x0efuture_feature\x18\x05 \x01(\x0e2$.goproto.protoc.features.EnumFeatureB+\x88\x01\x01\x98\x01\x01\x98\x01\x04\xa2\x01\v\x12\x06VALUE1\x18\x84\a\xa2\x01\v\x12\x06VALUE2\x18\xe9\a\xb2\x01\x03\b\xe9\aR\rfutureFeature\x12\x91\x01\n" +
+ "\x10unstable_feature\x18\a \x01(\x0e2$.goproto.protoc.features.EnumFeatureB@\x88\x01\x01\x98\x01\x01\x98\x01\x04\x98\x01\x03\x98\x01\x06\x98\x01\a\x98\x01\b\x98\x01\t\x98\x01\x05\x98\x01\x02\xa2\x01\v\x12\x06VALUE1\x18\x84\a\xa2\x01\v\x12\x06VALUE2\x18\x8fN\xb2\x01\x03\b\x8fNR\x0funstableFeatureX\x01*j\n" +
+ "\vEnumFeature\x12\x1d\n" +
+ "\x19TEST_ENUM_FEATURE_UNKNOWN\x10\x00\x12\n" +
+ "\n" +
+ "\x06VALUE1\x10\x01\x12\n" +
+ "\n" +
+ "\x06VALUE2\x10\x02\x12\n" +
+ "\n" +
+ "\x06VALUE3\x10\x03\x12\n" +
+ "\n" +
+ "\x06VALUE4\x10\x04\x12\n" +
+ "\n" +
+ "\x06VALUE5\x10\x050\x01:h\n" +
+ "\rtest_features\x12\x1b.google.protobuf.FeatureSet\x18\x8fN \x01(\v2%.goproto.protoc.features.TestFeaturesR\ftestFeaturesB@Z>google.golang.org/protobuf/cmd/protoc-gen-go/testdata/featuresb\beditionsp\xe9\a"
+
+var file_cmd_protoc_gen_go_testdata_features_test_features_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_cmd_protoc_gen_go_testdata_features_test_features_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_cmd_protoc_gen_go_testdata_features_test_features_proto_goTypes = []any{
+ (EnumFeature)(0), // 0: goproto.protoc.features.EnumFeature
+ (*TestFeatures)(nil), // 1: goproto.protoc.features.TestFeatures
+ (*descriptorpb.FeatureSet)(nil), // 2: google.protobuf.FeatureSet
+}
+var file_cmd_protoc_gen_go_testdata_features_test_features_proto_depIdxs = []int32{
+ 0, // 0: goproto.protoc.features.TestFeatures.enum_feature:type_name -> goproto.protoc.features.EnumFeature
+ 0, // 1: goproto.protoc.features.TestFeatures.source_feature:type_name -> goproto.protoc.features.EnumFeature
+ 0, // 2: goproto.protoc.features.TestFeatures.fixed_feature:type_name -> goproto.protoc.features.EnumFeature
+ 0, // 3: goproto.protoc.features.TestFeatures.future_feature:type_name -> goproto.protoc.features.EnumFeature
+ 0, // 4: goproto.protoc.features.TestFeatures.unstable_feature:type_name -> goproto.protoc.features.EnumFeature
+ 2, // 5: goproto.protoc.features.test_features:extendee -> google.protobuf.FeatureSet
+ 1, // 6: goproto.protoc.features.test_features:type_name -> goproto.protoc.features.TestFeatures
+ 7, // [7:7] is the sub-list for method output_type
+ 7, // [7:7] is the sub-list for method input_type
+ 6, // [6:7] is the sub-list for extension type_name
+ 5, // [5:6] is the sub-list for extension extendee
+ 0, // [0:5] is the sub-list for field type_name
+}
+
+func init() { file_cmd_protoc_gen_go_testdata_features_test_features_proto_init() }
+func file_cmd_protoc_gen_go_testdata_features_test_features_proto_init() {
+ if File_cmd_protoc_gen_go_testdata_features_test_features_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_cmd_protoc_gen_go_testdata_features_test_features_proto_rawDesc), len(file_cmd_protoc_gen_go_testdata_features_test_features_proto_rawDesc)),
+ NumEnums: 1,
+ NumMessages: 1,
+ NumExtensions: 1,
+ NumServices: 0,
+ },
+ GoTypes: file_cmd_protoc_gen_go_testdata_features_test_features_proto_goTypes,
+ DependencyIndexes: file_cmd_protoc_gen_go_testdata_features_test_features_proto_depIdxs,
+ EnumInfos: file_cmd_protoc_gen_go_testdata_features_test_features_proto_enumTypes,
+ MessageInfos: file_cmd_protoc_gen_go_testdata_features_test_features_proto_msgTypes,
+ ExtensionInfos: file_cmd_protoc_gen_go_testdata_features_test_features_proto_extTypes,
+ }.Build()
+ File_cmd_protoc_gen_go_testdata_features_test_features_proto = out.File
+ file_cmd_protoc_gen_go_testdata_features_test_features_proto_goTypes = nil
+ file_cmd_protoc_gen_go_testdata_features_test_features_proto_depIdxs = nil
+}
diff --git a/cmd/protoc-gen-go/testdata/features/test_features.proto b/cmd/protoc-gen-go/testdata/features/test_features.proto
new file mode 100644
index 0000000..6d58013
--- /dev/null
+++ b/cmd/protoc-gen-go/testdata/features/test_features.proto
@@ -0,0 +1,114 @@
+// 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.
+
+// This file contains a set of custom features for testing feature resolution of
+// arbitrary languages.
+
+edition = "2024";
+
+package goproto.protoc.features;
+
+import "google/protobuf/descriptor.proto";
+
+option go_package = "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/features";
+
+extend google.protobuf.FeatureSet {
+ TestFeatures test_features = 9999;
+}
+
+local message TestFeatures {
+ EnumFeature enum_feature = 1 [
+ retention = RETENTION_RUNTIME,
+ targets = TARGET_TYPE_FILE,
+ targets = TARGET_TYPE_FIELD,
+ targets = TARGET_TYPE_MESSAGE,
+ targets = TARGET_TYPE_ENUM,
+ targets = TARGET_TYPE_ENUM_ENTRY,
+ targets = TARGET_TYPE_SERVICE,
+ targets = TARGET_TYPE_METHOD,
+ targets = TARGET_TYPE_ONEOF,
+ targets = TARGET_TYPE_EXTENSION_RANGE,
+ feature_support.edition_introduced = EDITION_2023,
+ edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
+ edition_defaults = { edition: EDITION_2023, value: "VALUE2" },
+ edition_defaults = { edition: EDITION_2024, value: "VALUE3" }
+ ];
+
+ bool bool_feature = 2 [
+ retention = RETENTION_RUNTIME,
+ targets = TARGET_TYPE_FILE,
+ targets = TARGET_TYPE_FIELD,
+ targets = TARGET_TYPE_MESSAGE,
+ targets = TARGET_TYPE_ENUM,
+ targets = TARGET_TYPE_ENUM_ENTRY,
+ targets = TARGET_TYPE_SERVICE,
+ targets = TARGET_TYPE_METHOD,
+ targets = TARGET_TYPE_ONEOF,
+ targets = TARGET_TYPE_EXTENSION_RANGE,
+ feature_support.edition_introduced = EDITION_2023,
+ edition_defaults = { edition: EDITION_LEGACY, value: "false" },
+ edition_defaults = { edition: EDITION_2023, value: "true" }
+ ];
+
+ EnumFeature source_feature = 3 [
+ retention = RETENTION_SOURCE,
+ targets = TARGET_TYPE_FILE,
+ targets = TARGET_TYPE_FIELD,
+ targets = TARGET_TYPE_MESSAGE,
+ targets = TARGET_TYPE_ENUM,
+ targets = TARGET_TYPE_ENUM_ENTRY,
+ targets = TARGET_TYPE_SERVICE,
+ targets = TARGET_TYPE_METHOD,
+ targets = TARGET_TYPE_ONEOF,
+ targets = TARGET_TYPE_EXTENSION_RANGE,
+ feature_support.edition_introduced = EDITION_2023,
+ edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
+ ];
+
+ EnumFeature fixed_feature = 4 [
+ retention = RETENTION_RUNTIME,
+ targets = TARGET_TYPE_FILE,
+ targets = TARGET_TYPE_FIELD,
+ feature_support = {
+ edition_introduced: EDITION_2023
+ edition_removed: EDITION_2023
+ },
+ edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
+ edition_defaults = { edition: EDITION_2023, value: "VALUE2" }
+ ];
+
+ EnumFeature future_feature = 5 [
+ retention = RETENTION_RUNTIME,
+ targets = TARGET_TYPE_FILE,
+ targets = TARGET_TYPE_FIELD,
+ feature_support = { edition_introduced: EDITION_2024 },
+ edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
+ edition_defaults = { edition: EDITION_2024, value: "VALUE2" }
+ ];
+
+ EnumFeature unstable_feature = 7 [
+ retention = RETENTION_RUNTIME,
+ targets = TARGET_TYPE_FILE,
+ targets = TARGET_TYPE_FIELD,
+ targets = TARGET_TYPE_MESSAGE,
+ targets = TARGET_TYPE_ENUM,
+ targets = TARGET_TYPE_ENUM_ENTRY,
+ targets = TARGET_TYPE_SERVICE,
+ targets = TARGET_TYPE_METHOD,
+ targets = TARGET_TYPE_ONEOF,
+ targets = TARGET_TYPE_EXTENSION_RANGE,
+ feature_support.edition_introduced = EDITION_UNSTABLE,
+ edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
+ edition_defaults = { edition: EDITION_UNSTABLE, value: "VALUE2" }
+ ];
+}
+
+local enum EnumFeature {
+ TEST_ENUM_FEATURE_UNKNOWN = 0;
+ VALUE1 = 1;
+ VALUE2 = 2;
+ VALUE3 = 3;
+ VALUE4 = 4;
+ VALUE5 = 5;
+}
diff --git a/cmd/protoc-gen-go/testdata/gen_test.go b/cmd/protoc-gen-go/testdata/gen_test.go
index 4c7e930..d340c53 100644
--- a/cmd/protoc-gen-go/testdata/gen_test.go
+++ b/cmd/protoc-gen-go/testdata/gen_test.go
@@ -14,6 +14,8 @@
_ "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/extensions/ext"
_ "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/extensions/extra"
_ "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/extensions/proto3"
+ _ "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/featureresolution"
+ _ "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/features"
_ "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/fieldnames"
_ "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/import_option"
_ "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/import_option_custom"
diff --git a/compiler/protogen/protogen.go b/compiler/protogen/protogen.go
index f68418b..9e967e1 100644
--- a/compiler/protogen/protogen.go
+++ b/compiler/protogen/protogen.go
@@ -28,6 +28,7 @@
"strings"

"google.golang.org/protobuf/encoding/prototext"
+ "google.golang.org/protobuf/internal/editiondefaults"
"google.golang.org/protobuf/internal/filedesc"
"google.golang.org/protobuf/internal/genid"
"google.golang.org/protobuf/internal/strs"
@@ -113,15 +114,16 @@
SupportedEditionsMinimum descriptorpb.Edition
SupportedEditionsMaximum descriptorpb.Edition

- fileReg *protoregistry.Files
- enumsByName map[protoreflect.FullName]*Enum
- messagesByName map[protoreflect.FullName]*Message
- annotateCode bool
- pathType pathType
- module string
- genFiles []*GeneratedFile
- opts Options
- err error
+ featureSetDefaults *descriptorpb.FeatureSetDefaults
+ fileReg *protoregistry.Files
+ enumsByName map[protoreflect.FullName]*Enum
+ messagesByName map[protoreflect.FullName]*Message
+ annotateCode bool
+ pathType pathType
+ module string
+ genFiles []*GeneratedFile
+ opts Options
+ err error
}

type Options struct {
@@ -155,6 +157,13 @@
// for this package.
ImportRewriteFunc func(GoImportPath) GoImportPath

+ // A custom FeatureSetDefaults to use instead of the compiled-in defaults.
+ // If nil, the compiled-in go defaults are used.
+ //
+ // This is useful to override for plugins that need to use other languages' features. It can be
+ // produced by using protoc or the compile_edition_defaults bazel rule.
+ FeatureSetDefaults *descriptorpb.FeatureSetDefaults
+
// StripForEditionsDiff true means that the plugin will not emit certain
// parts of the generated code in order to make it possible to compare a
// proto2/proto3 file with its equivalent (according to proto spec)
@@ -183,6 +192,15 @@
opts: opts,
}

+ if opts.FeatureSetDefaults != nil {
+ gen.featureSetDefaults = proto.Clone(opts.FeatureSetDefaults).(*descriptorpb.FeatureSetDefaults)
+ } else {
+ gen.featureSetDefaults = &descriptorpb.FeatureSetDefaults{}
+ if err := proto.Unmarshal(editiondefaults.Defaults, gen.featureSetDefaults); err != nil {
+ return nil, fmt.Errorf("unmarshal editions defaults: %v", err)
+ }
+ }
+
packageNames := make(map[string]GoPackageName) // filename -> package name
importPaths := make(map[string]GoImportPath) // filename -> import path
apiLevel := make(map[string]gofeaturespb.GoFeatures_APILevel) // filename -> api level
@@ -518,6 +536,38 @@
return resp
}

+func (gen *Plugin) getDefaultFeatures(fileDesc *descriptorpb.FileDescriptorProto) (*descriptorpb.FeatureSet, error) {
+ defaults := gen.featureSetDefaults
+ var edition descriptorpb.Edition
+ switch fileDesc.GetSyntax() {
+ case "editions":
+ edition = fileDesc.GetEdition()
+ case "proto3":
+ edition = descriptorpb.Edition_EDITION_PROTO3
+ default:
+ edition = descriptorpb.Edition_EDITION_PROTO2
+ }
+ if edition < defaults.GetMinimumEdition() {
+ return nil, fmt.Errorf("edition %v is lower than the minimum supported edition %v", edition, defaults.GetMinimumEdition())
+ }
+ if edition > defaults.GetMaximumEdition() && edition != descriptorpb.Edition_EDITION_UNSTABLE {
+ return nil, fmt.Errorf("edition %v is greater than the maximum supported edition %v", edition, defaults.GetMaximumEdition())
+ }
+ var match *descriptorpb.FeatureSetDefaults_FeatureSetEditionDefault
+ for _, d := range gen.featureSetDefaults.GetDefaults() {
+ if d.GetEdition().Number() > edition.Number() {
+ break
+ }
+ match = d
+ }
+ if match == nil {
+ return nil, fmt.Errorf("edition %v does not have a default FeatureSet supplied", edition)
+ }
+ result := proto.Clone(match.GetOverridableFeatures()).(*descriptorpb.FeatureSet)
+ proto.Merge(result, match.GetFixedFeatures())
+ return result, nil
+}
+
// A File describes a .proto source file.
type File struct {
Desc protoreflect.FileDescriptor
@@ -532,6 +582,8 @@
Extensions []*Extension // top-level extension declarations
Services []*Service // top-level service declarations

+ ResolvedFeatures *descriptorpb.FeatureSet // resolved features for this file
+
Generate bool // true if we should generate code for this file

// GeneratedFilenamePrefix is used to construct filenames for generated
@@ -559,12 +611,18 @@
if apiLevel != gofeaturespb.GoFeatures_API_LEVEL_UNSPECIFIED {
defaultAPILevel = apiLevel
}
+ features, err := gen.getDefaultFeatures(p)
+ if err != nil {
+ return nil, err
+ }
+ proto.Merge(features, p.GetOptions().GetFeatures())
f := &File{
- Desc: desc,
- Proto: p,
- GoPackageName: packageName,
- GoImportPath: importPath,
- location: Location{SourceFile: desc.Path()},
+ Desc: desc,
+ Proto: p,
+ GoPackageName: packageName,
+ GoImportPath: importPath,
+ ResolvedFeatures: features,
+ location: Location{SourceFile: desc.Path()},

APILevel: fileAPILevel(desc, defaultAPILevel),
}
@@ -595,7 +653,7 @@
f.Messages = append(f.Messages, newMessage(gen, f, nil, mds.Get(i)))
}
for i, xds := 0, desc.Extensions(); i < xds.Len(); i++ {
- f.Extensions = append(f.Extensions, newField(gen, f, nil, xds.Get(i)))
+ f.Extensions = append(f.Extensions, newField(gen, f, nil, nil, xds.Get(i)))
}
for i, sds := 0, desc.Services(); i < sds.Len(); i++ {
f.Services = append(f.Services, newService(gen, f, sds.Get(i)))
@@ -639,20 +697,30 @@

Location Location // location of this enum
Comments CommentSet // comments associated with this enum
+
+ ResolvedFeatures *descriptorpb.FeatureSet // resolved features for this enum
}

func newEnum(gen *Plugin, f *File, parent *Message, desc protoreflect.EnumDescriptor) *Enum {
var loc Location
+ var features *descriptorpb.FeatureSet
if parent != nil {
loc = parent.Location.appendPath(genid.DescriptorProto_EnumType_field_number, desc.Index())
+ features = parent.ResolvedFeatures
} else {
loc = f.location.appendPath(genid.FileDescriptorProto_EnumType_field_number, desc.Index())
+ features = f.ResolvedFeatures
+ }
+ if desc.Options().(*descriptorpb.EnumOptions).GetFeatures() != nil {
+ features = proto.Clone(features).(*descriptorpb.FeatureSet)
+ proto.Merge(features, desc.Options().(*descriptorpb.EnumOptions).GetFeatures())
}
enum := &Enum{
- Desc: desc,
- GoIdent: newGoIdent(f, desc),
- Location: loc,
- Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ Desc: desc,
+ GoIdent: newGoIdent(f, desc),
+ Location: loc,
+ Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ ResolvedFeatures: features,
}
gen.enumsByName[desc.FullName()] = enum
for i, vds := 0, enum.Desc.Values(); i < vds.Len(); i++ {
@@ -677,6 +745,8 @@

Location Location // location of this enum value
Comments CommentSet // comments associated with this enum value
+
+ ResolvedFeatures *descriptorpb.FeatureSet // resolved features for this enum value
}

func newEnumValue(gen *Plugin, f *File, message *Message, enum *Enum, desc protoreflect.EnumValueDescriptor) *EnumValue {
@@ -718,12 +788,18 @@
name = parentIdent.GoName + "_" + strs.TrimEnumPrefix(string(desc.Name()), prefix)
}
}
+ features := enum.ResolvedFeatures
+ if desc.Options().(*descriptorpb.EnumValueOptions).GetFeatures() != nil {
+ features = proto.Clone(features).(*descriptorpb.FeatureSet)
+ proto.Merge(features, desc.Options().(*descriptorpb.EnumValueOptions).GetFeatures())
+ }
ev := &EnumValue{
- Desc: desc,
- GoIdent: f.GoImportPath.Ident(name),
- Parent: enum,
- Location: loc,
- Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ Desc: desc,
+ GoIdent: f.GoImportPath.Ident(name),
+ Parent: enum,
+ Location: loc,
+ Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ ResolvedFeatures: features,
}
if prefixedName != "" {
ev.PrefixedAlias = f.GoImportPath.Ident(prefixedName)
@@ -747,16 +823,21 @@
Location Location // location of this message
Comments CommentSet // comments associated with this message

+ ResolvedFeatures *descriptorpb.FeatureSet // resolved features for this message
+
// APILevel specifies which API to generate. One of OPEN, HYBRID or OPAQUE.
APILevel gofeaturespb.GoFeatures_APILevel
}

func newMessage(gen *Plugin, f *File, parent *Message, desc protoreflect.MessageDescriptor) *Message {
var loc Location
+ var features *descriptorpb.FeatureSet
if parent != nil {
loc = parent.Location.appendPath(genid.DescriptorProto_NestedType_field_number, desc.Index())
+ features = parent.ResolvedFeatures
} else {
loc = f.location.appendPath(genid.FileDescriptorProto_MessageType_field_number, desc.Index())
+ features = f.ResolvedFeatures
}

def := f.APILevel
@@ -765,11 +846,16 @@
def = parent.APILevel
}

+ if desc.Options().(*descriptorpb.MessageOptions).GetFeatures() != nil {
+ features = proto.Clone(features).(*descriptorpb.FeatureSet)
+ proto.Merge(features, desc.Options().(*descriptorpb.MessageOptions).GetFeatures())
+ }
message := &Message{
- Desc: desc,
- GoIdent: newGoIdent(f, desc),
- Location: loc,
- Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ Desc: desc,
+ GoIdent: newGoIdent(f, desc),
+ Location: loc,
+ Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ ResolvedFeatures: features,

APILevel: messageAPILevel(desc, def),
}
@@ -780,14 +866,18 @@
for i, mds := 0, desc.Messages(); i < mds.Len(); i++ {
message.Messages = append(message.Messages, newMessage(gen, f, message, mds.Get(i)))
}
- for i, fds := 0, desc.Fields(); i < fds.Len(); i++ {
- message.Fields = append(message.Fields, newField(gen, f, message, fds.Get(i)))
- }
for i, ods := 0, desc.Oneofs(); i < ods.Len(); i++ {
message.Oneofs = append(message.Oneofs, newOneof(gen, f, message, ods.Get(i)))
}
+ for i, fds := 0, desc.Fields(); i < fds.Len(); i++ {
+ var oneof *Oneof
+ if fds.Get(i).ContainingOneof() != nil {
+ oneof = message.Oneofs[fds.Get(i).ContainingOneof().Index()]
+ }
+ message.Fields = append(message.Fields, newField(gen, f, message, oneof, fds.Get(i)))
+ }
for i, xds := 0, desc.Extensions(); i < xds.Len(); i++ {
- message.Extensions = append(message.Extensions, newField(gen, f, message, xds.Get(i)))
+ message.Extensions = append(message.Extensions, newField(gen, f, message, nil, xds.Get(i)))
}

// Resolve local references between fields and oneofs.
@@ -918,6 +1008,8 @@
Location Location // location of this field
Comments CommentSet // comments associated with this field

+ ResolvedFeatures *descriptorpb.FeatureSet // resolved features for the field
+
// camelCase is the same as GoName, but without the name
// mangling. This is used in builders, where only the single
// name "Build" needs to be mangled.
@@ -931,21 +1023,33 @@
hasConflictHybrid bool
}

-func newField(gen *Plugin, f *File, message *Message, desc protoreflect.FieldDescriptor) *Field {
+func newField(gen *Plugin, f *File, message *Message, oneof *Oneof, desc protoreflect.FieldDescriptor) *Field {
var loc Location
+ var features *descriptorpb.FeatureSet
switch {
case desc.IsExtension() && message == nil:
loc = f.location.appendPath(genid.FileDescriptorProto_Extension_field_number, desc.Index())
+ features = f.ResolvedFeatures
case desc.IsExtension() && message != nil:
loc = message.Location.appendPath(genid.DescriptorProto_Extension_field_number, desc.Index())
+ features = message.ResolvedFeatures
default:
loc = message.Location.appendPath(genid.DescriptorProto_Field_field_number, desc.Index())
+ if oneof != nil {
+ features = oneof.ResolvedFeatures
+ } else {
+ features = message.ResolvedFeatures
+ }
}
camelCased := strs.GoCamelCase(string(desc.Name()))
var parentPrefix string
if message != nil {
parentPrefix = message.GoIdent.GoName + "_"
}
+ if desc.Options().(*descriptorpb.FieldOptions).GetFeatures() != nil {
+ features = proto.Clone(features).(*descriptorpb.FeatureSet)
+ proto.Merge(features, desc.Options().(*descriptorpb.FieldOptions).GetFeatures())
+ }
field := &Field{
Desc: desc,
GoName: camelCased,
@@ -953,9 +1057,10 @@
GoImportPath: f.GoImportPath,
GoName: parentPrefix + camelCased,
},
- Parent: message,
- Location: loc,
- Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ Parent: message,
+ Location: loc,
+ Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ ResolvedFeatures: features,
}

opaqueNewFieldHook(desc, field)
@@ -1011,6 +1116,8 @@
Location Location // location of this oneof
Comments CommentSet // comments associated with this oneof

+ ResolvedFeatures *descriptorpb.FeatureSet // resolved features for the oneof
+
// camelCase is the same as GoName, but without the name mangling.
// This is used in builders, which never have their names mangled
camelCase string
@@ -1027,6 +1134,11 @@
loc := message.Location.appendPath(genid.DescriptorProto_OneofDecl_field_number, desc.Index())
camelCased := strs.GoCamelCase(string(desc.Name()))
parentPrefix := message.GoIdent.GoName + "_"
+ features := message.ResolvedFeatures
+ if desc.Options().(*descriptorpb.OneofOptions).GetFeatures() != nil {
+ features = proto.Clone(features).(*descriptorpb.FeatureSet)
+ proto.Merge(features, desc.Options().(*descriptorpb.OneofOptions).GetFeatures())
+ }
oneof := &Oneof{
Desc: desc,
Parent: message,
@@ -1035,8 +1147,9 @@
GoImportPath: f.GoImportPath,
GoName: parentPrefix + camelCased,
},
- Location: loc,
- Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ Location: loc,
+ Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ ResolvedFeatures: features,
}

opaqueNewOneofHook(desc, oneof)
@@ -1057,15 +1170,23 @@

Location Location // location of this service
Comments CommentSet // comments associated with this service
+
+ ResolvedFeatures *descriptorpb.FeatureSet // resolved features for the service
}

func newService(gen *Plugin, f *File, desc protoreflect.ServiceDescriptor) *Service {
loc := f.location.appendPath(genid.FileDescriptorProto_Service_field_number, desc.Index())
+ features := f.ResolvedFeatures
+ if desc.Options().(*descriptorpb.ServiceOptions).GetFeatures() != nil {
+ features = proto.Clone(features).(*descriptorpb.FeatureSet)
+ proto.Merge(features, desc.Options().(*descriptorpb.ServiceOptions).GetFeatures())
+ }
service := &Service{
- Desc: desc,
- GoName: strs.GoCamelCase(string(desc.Name())),
- Location: loc,
- Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ Desc: desc,
+ GoName: strs.GoCamelCase(string(desc.Name())),
+ Location: loc,
+ Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ ResolvedFeatures: features,
}
for i, mds := 0, desc.Methods(); i < mds.Len(); i++ {
service.Methods = append(service.Methods, newMethod(gen, f, service, mds.Get(i)))
@@ -1086,16 +1207,24 @@

Location Location // location of this method
Comments CommentSet // comments associated with this method
+
+ ResolvedFeatures *descriptorpb.FeatureSet // resolved features for the service
}

func newMethod(gen *Plugin, f *File, service *Service, desc protoreflect.MethodDescriptor) *Method {
loc := service.Location.appendPath(genid.ServiceDescriptorProto_Method_field_number, desc.Index())
+ features := service.ResolvedFeatures
+ if desc.Options().(*descriptorpb.MethodOptions).GetFeatures() != nil {
+ features = proto.Clone(features).(*descriptorpb.FeatureSet)
+ proto.Merge(features, desc.Options().(*descriptorpb.MethodOptions).GetFeatures())
+ }
method := &Method{
- Desc: desc,
- GoName: strs.GoCamelCase(string(desc.Name())),
- Parent: service,
- Location: loc,
- Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ Desc: desc,
+ GoName: strs.GoCamelCase(string(desc.Name())),
+ Parent: service,
+ Location: loc,
+ Comments: makeCommentSet(gen, f.Desc.SourceLocations().ByDescriptor(desc)),
+ ResolvedFeatures: features,
}
return method
}
diff --git a/internal/cmd/generate-protos/main.go b/internal/cmd/generate-protos/main.go
index 6ce8435..27b05a3 100644
--- a/internal/cmd/generate-protos/main.go
+++ b/internal/cmd/generate-protos/main.go
@@ -305,8 +305,16 @@

func generateEditionsDefaults() {
dest := filepath.Join(repoRoot, "internal", "editiondefaults", "editions_defaults.binpb")
+ features := filepath.Join(repoRoot, "src", "google", "protobuf", "go_features.proto")
+ generateFeaturesEditionsDefaults(features, dest)
+
+ dest = filepath.Join(repoRoot, "cmd", "protoc-gen-go", "feature_resolution_test", "test_features_defaults.binpb")
+ features = filepath.Join(repoRoot, "cmd", "protoc-gen-go", "testdata", "features", "test_features.proto")
+ generateFeaturesEditionsDefaults(features, dest)
+}
+
+func generateFeaturesEditionsDefaults(features string, dest string) {
srcDescriptorProto := filepath.Join(protoRoot, "src", "google", "protobuf", "descriptor.proto")
- srcGoFeatures := filepath.Join(repoRoot, "src", "google", "protobuf", "go_features.proto")
// The enum in Go string formats to "EDITION_${EDITION}" but protoc expects
// the flag in the form "${EDITION}". To work around this, we trim the
// "EDITION_" prefix.
@@ -318,7 +326,8 @@
"--edition_defaults_minimum", minEdition,
"--edition_defaults_maximum", maxEdition,
"-I"+filepath.Join(protoRoot, "src"), "-I"+filepath.Join(repoRoot, "src"),
- srcDescriptorProto, srcGoFeatures,
+ "-I"+repoRoot,
+ srcDescriptorProto, features,
)
out, err := cmd.CombinedOutput()
if err != nil {

Change information

Files:
  • A cmd/protoc-gen-go/feature_resolution_test/feature_resolution_test.go
  • A cmd/protoc-gen-go/feature_resolution_test/test_features_defaults.binpb
  • A cmd/protoc-gen-go/testdata/featureresolution/basic.pb.go
  • A cmd/protoc-gen-go/testdata/featureresolution/basic.proto
  • A cmd/protoc-gen-go/testdata/features/test_features.pb.go
  • A cmd/protoc-gen-go/testdata/features/test_features.proto
  • M cmd/protoc-gen-go/testdata/gen_test.go
  • M compiler/protogen/protogen.go
  • M internal/cmd/generate-protos/main.go
Change size: XL
Delta: 9 files changed, 1916 insertions(+), 49 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: protobuf
Gerrit-Branch: master
Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
Gerrit-Change-Number: 729461
Gerrit-PatchSet: 1
Gerrit-Owner: Mike Kruskal <mkru...@google.com>
unsatisfied_requirement
satisfied_requirement
open
diffy

Lasse Folger (Gerrit)

unread,
Dec 12, 2025, 2:24:41 AM (6 days ago) Dec 12
to Mike Kruskal, goph...@pubsubhelper.golang.org, Michael Stapelberg, golang-co...@googlegroups.com
Attention needed from Michael Stapelberg and Mike Kruskal

Lasse Folger voted and added 1 comment

Votes added by Lasse Folger

Code-Review+1

1 comment

File cmd/protoc-gen-go/feature_resolution_test/test_features_defaults.binpb
File-level comment, Patchset 1 (Latest):
Lasse Folger . unresolved

How did you generate this file?

Could this be integrated into https://github.com/protocolbuffers/protobuf-go/blob/master/internal/cmd/generate-protos/main.go#L306-L328

or a separate function in that file which is then called from main?

Open in Gerrit

Related details

Attention is currently required from:
  • Michael Stapelberg
  • Mike Kruskal
Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: protobuf
    Gerrit-Branch: master
    Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
    Gerrit-Change-Number: 729461
    Gerrit-PatchSet: 1
    Gerrit-Owner: Mike Kruskal <mkru...@google.com>
    Gerrit-Reviewer: Lasse Folger <lasse...@google.com>
    Gerrit-Reviewer: Michael Stapelberg <stape...@google.com>
    Gerrit-Attention: Michael Stapelberg <stape...@google.com>
    Gerrit-Attention: Mike Kruskal <mkru...@google.com>
    Gerrit-Comment-Date: Fri, 12 Dec 2025 07:24:33 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: Yes
    unsatisfied_requirement
    satisfied_requirement
    open
    diffy

    Lasse Folger (Gerrit)

    unread,
    Dec 12, 2025, 10:28:49 AM (6 days ago) Dec 12
    to Mike Kruskal, goph...@pubsubhelper.golang.org, Michael Stapelberg, golang-co...@googlegroups.com
    Attention needed from Michael Stapelberg and Mike Kruskal

    Lasse Folger added 1 comment

    File cmd/protoc-gen-go/feature_resolution_test/test_features_defaults.binpb
    Lasse Folger . resolved

    How did you generate this file?

    Could this be integrated into https://github.com/protocolbuffers/protobuf-go/blob/master/internal/cmd/generate-protos/main.go#L306-L328

    or a separate function in that file which is then called from main?

    Lasse Folger

    Sorry, I missed that you already did this (I blame my lack of sleep for this).

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Michael Stapelberg
    • Mike Kruskal
    Submit Requirements:
      • requirement is not satisfiedCode-Review
      • requirement satisfiedNo-Unresolved-Comments
      • requirement satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: protobuf
      Gerrit-Branch: master
      Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
      Gerrit-Change-Number: 729461
      Gerrit-PatchSet: 1
      Gerrit-Owner: Mike Kruskal <mkru...@google.com>
      Gerrit-Reviewer: Lasse Folger <lasse...@google.com>
      Gerrit-Reviewer: Michael Stapelberg <stape...@google.com>
      Gerrit-Attention: Michael Stapelberg <stape...@google.com>
      Gerrit-Attention: Mike Kruskal <mkru...@google.com>
      Gerrit-Comment-Date: Fri, 12 Dec 2025 15:28:40 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      Comment-In-Reply-To: Lasse Folger <lasse...@google.com>
      unsatisfied_requirement
      satisfied_requirement
      open
      diffy

      Michael Stapelberg (Gerrit)

      unread,
      Dec 12, 2025, 10:29:18 AM (6 days ago) Dec 12
      to Mike Kruskal, goph...@pubsubhelper.golang.org, Lasse Folger, golang-co...@googlegroups.com
      Attention needed from Mike Kruskal

      Michael Stapelberg voted

      Code-Review+2
      Commit-Queue+1
      Open in Gerrit

      Related details

      Attention is currently required from:
      • Mike Kruskal
      Submit Requirements:
      • requirement satisfiedCode-Review
      • requirement satisfiedNo-Unresolved-Comments
      • requirement satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: protobuf
      Gerrit-Branch: master
      Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
      Gerrit-Change-Number: 729461
      Gerrit-PatchSet: 1
      Gerrit-Owner: Mike Kruskal <mkru...@google.com>
      Gerrit-Reviewer: Lasse Folger <lasse...@google.com>
      Gerrit-Reviewer: Michael Stapelberg <stape...@google.com>
      Gerrit-Attention: Mike Kruskal <mkru...@google.com>
      Gerrit-Comment-Date: Fri, 12 Dec 2025 15:29:12 +0000
      Gerrit-HasComments: No
      Gerrit-Has-Labels: Yes
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Michael Stapelberg (Gerrit)

      unread,
      Dec 12, 2025, 10:46:42 AM (6 days ago) Dec 12
      to Mike Kruskal, goph...@pubsubhelper.golang.org, Go LUCI, Lasse Folger, golang-co...@googlegroups.com
      Attention needed from Mike Kruskal

      Michael Stapelberg added 1 comment

      Patchset-level comments
      Michael Stapelberg . resolved

      Oh, I think this is failing on LUCI because we manually manage protoc. I can take care of that on Monday morning.

      Open in Gerrit

      Related details

      Attention is currently required from:
      • Mike Kruskal
      Submit Requirements:
      • requirement satisfiedCode-Review
      • requirement satisfiedNo-Unresolved-Comments
      • requirement satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: protobuf
      Gerrit-Branch: master
      Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
      Gerrit-Change-Number: 729461
      Gerrit-PatchSet: 1
      Gerrit-Owner: Mike Kruskal <mkru...@google.com>
      Gerrit-Reviewer: Lasse Folger <lasse...@google.com>
      Gerrit-Reviewer: Michael Stapelberg <stape...@google.com>
      Gerrit-Attention: Mike Kruskal <mkru...@google.com>
      Gerrit-Comment-Date: Fri, 12 Dec 2025 15:46:36 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Michael Stapelberg (Gerrit)

      unread,
      Dec 15, 2025, 10:03:04 AM (3 days ago) Dec 15
      to Mike Kruskal, goph...@pubsubhelper.golang.org, Go LUCI, Lasse Folger, golang-co...@googlegroups.com
      Attention needed from Mike Kruskal

      Michael Stapelberg voted Commit-Queue+1

      Commit-Queue+1
      Open in Gerrit

      Related details

      Attention is currently required from:
      • Mike Kruskal
      Submit Requirements:
      • requirement satisfiedCode-Review
      • requirement satisfiedNo-Unresolved-Comments
      • requirement satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: protobuf
      Gerrit-Branch: master
      Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
      Gerrit-Change-Number: 729461
      Gerrit-PatchSet: 1
      Gerrit-Owner: Mike Kruskal <mkru...@google.com>
      Gerrit-Reviewer: Lasse Folger <lasse...@google.com>
      Gerrit-Reviewer: Michael Stapelberg <stape...@google.com>
      Gerrit-Attention: Mike Kruskal <mkru...@google.com>
      Gerrit-Comment-Date: Mon, 15 Dec 2025 15:02:55 +0000
      Gerrit-HasComments: No
      Gerrit-Has-Labels: Yes
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Mike Kruskal (Gerrit)

      unread,
      Dec 16, 2025, 11:59:06 AM (2 days ago) Dec 16
      to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
      Attention needed from Mike Kruskal

      Mike Kruskal uploaded new patchset

      Mike Kruskal uploaded patch set #2 to this change.
      Following approvals got outdated and were removed:
      • TryBots-Pass: LUCI-TryBot-Result+1 by Go LUCI
      Open in Gerrit

      Related details

      Attention is currently required from:
      • Mike Kruskal
      Submit Requirements:
      • requirement satisfiedCode-Review
      • requirement satisfiedNo-Unresolved-Comments
      • requirement satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: newpatchset
      Gerrit-Project: protobuf
      Gerrit-Branch: master
      Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
      Gerrit-Change-Number: 729461
      Gerrit-PatchSet: 2
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Michael Stapelberg (Gerrit)

      unread,
      Dec 17, 2025, 3:43:23 AM (yesterday) Dec 17
      to Mike Kruskal, goph...@pubsubhelper.golang.org, Go LUCI, Lasse Folger, golang-co...@googlegroups.com
      Attention needed from Mike Kruskal

      Michael Stapelberg voted

      Code-Review+2
      Commit-Queue+1
      Open in Gerrit

      Related details

      Attention is currently required from:
      • Mike Kruskal
      Submit Requirements:
      • requirement satisfiedCode-Review
      • requirement satisfiedNo-Unresolved-Comments
      • requirement satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: protobuf
      Gerrit-Branch: master
      Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
      Gerrit-Change-Number: 729461
      Gerrit-PatchSet: 2
      Gerrit-Owner: Mike Kruskal <mkru...@google.com>
      Gerrit-Reviewer: Lasse Folger <lasse...@google.com>
      Gerrit-Reviewer: Michael Stapelberg <stape...@google.com>
      Gerrit-Attention: Mike Kruskal <mkru...@google.com>
      Gerrit-Comment-Date: Wed, 17 Dec 2025 08:43:15 +0000
      Gerrit-HasComments: No
      Gerrit-Has-Labels: Yes
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Michael Stapelberg (Gerrit)

      unread,
      Dec 17, 2025, 3:52:13 AM (yesterday) Dec 17
      to Mike Kruskal, goph...@pubsubhelper.golang.org, golang-...@googlegroups.com, Go LUCI, Lasse Folger, golang-co...@googlegroups.com

      Michael Stapelberg submitted the change

      Change information

      Commit message:
      compiler/protogen: introduce and expose feature resoluton to protogen

      This will allow go plugins to specify any custom extensions they care about using protoc to generate a FeatureSetDefaults object. They can then access all ResolvedFeatures of any descriptors to do more involved codegen for languages outside of go.
      Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
      Reviewed-by: Lasse Folger <lasse...@google.com>
      Reviewed-by: Michael Stapelberg <stape...@google.com>
      Files:
      • A cmd/protoc-gen-go/feature_resolution_test/feature_resolution_test.go
      • A cmd/protoc-gen-go/feature_resolution_test/test_features_defaults.binpb
      • A cmd/protoc-gen-go/testdata/featureresolution/basic.pb.go
      • A cmd/protoc-gen-go/testdata/featureresolution/basic.proto
      • A cmd/protoc-gen-go/testdata/features/test_features.pb.go
      • A cmd/protoc-gen-go/testdata/features/test_features.proto
      • M cmd/protoc-gen-go/testdata/gen_test.go
      • M compiler/protogen/protogen.go
      • M internal/cmd/generate-protos/main.go
      Change size: XL
      Delta: 9 files changed, 1916 insertions(+), 49 deletions(-)
      Branch: refs/heads/master
      Submit Requirements:
      • requirement satisfiedCode-Review: +1 by Lasse Folger, +2 by Michael Stapelberg
      • requirement satisfiedTryBots-Pass: LUCI-TryBot-Result+1 by Go LUCI
      Open in Gerrit
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: merged
      Gerrit-Project: protobuf
      Gerrit-Branch: master
      Gerrit-Change-Id: I3be49645612c6712a9e58ba65b6374735f8197b8
      Gerrit-Change-Number: 729461
      Gerrit-PatchSet: 3
      open
      diffy
      satisfied_requirement
      Reply all
      Reply to author
      Forward
      0 new messages