Skip to content

Commit 450060f

Browse files
Sirius7Cangshuclaude
authored andcommitted
Add support for Go features (features.(pb.go).*) in buf
This commit adds support for configuring Go features in buf.yaml for managed mode code generation and breaking change detection. Changes: - Add ResolveGoFeature and ResolveGoFeatureForEnum functions in customfeatures - Add FIELD_SAME_GO_STRIP_ENUM_PREFIX breaking change rule - Add FileOptionGoApiLevel for managed mode (features.(pb.go).api_level) - Add test for ResolveGoFeature The Go features use the external google.golang.org/protobuf/types/gofeaturespb package to avoid namespace conflicts. Note: The strip_enum_prefix breaking rule test is skipped as it requires edition 2024 which is not yet fully supported in buf. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 09cc071 commit 450060f

File tree

7 files changed

+147
-5
lines changed

7 files changed

+147
-5
lines changed

private/bufpkg/bufcheck/breaking_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,18 @@ func TestRunBreakingFieldSameJavaUTF8Validation(t *testing.T) {
379379
)
380380
}
381381

382+
func TestRunBreakingFieldSameGoStripEnumPrefix(t *testing.T) {
383+
t.Skip("TODO: enable when edition 2024 is supported")
384+
t.Parallel()
385+
testBreaking(
386+
t,
387+
"breaking_field_same_go_strip_enum_prefix",
388+
bufanalysistesting.NewFileAnnotation(t, "1.proto", 8, 1, 8, 7, "FIELD_SAME_GO_STRIP_ENUM_PREFIX"),
389+
bufanalysistesting.NewFileAnnotation(t, "1.proto", 15, 1, 15, 12, "FIELD_SAME_GO_STRIP_ENUM_PREFIX"),
390+
bufanalysistesting.NewFileAnnotation(t, "1.proto", 22, 1, 22, 17, "FIELD_SAME_GO_STRIP_ENUM_PREFIX"),
391+
)
392+
}
393+
382394
func TestRunBreakingFieldSameDefault(t *testing.T) {
383395
t.Parallel()
384396
testBreaking(

private/bufpkg/bufcheck/bufcheckserver/internal/bufcheckserverbuild/bufcheckserverbuild.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ var (
141141
Type: check.RuleTypeBreaking,
142142
Handler: bufcheckserverhandle.HandleBreakingFieldSameJavaUTF8Validation,
143143
}
144+
// BreakingFieldSameGoStripEnumPrefixRuleSpecBuilder is a rule spec builder.
145+
BreakingFieldSameGoStripEnumPrefixRuleSpecBuilder = &bufcheckserverutil.RuleSpecBuilder{
146+
ID: "FIELD_SAME_GO_STRIP_ENUM_PREFIX",
147+
Purpose: "Checks that enums have the same Go strip enum prefix, based on (pb.go).strip_enum_prefix feature.",
148+
Type: check.RuleTypeBreaking,
149+
Handler: bufcheckserverhandle.HandleBreakingFieldSameGoStripEnumPrefix,
150+
}
144151
// BreakingFieldSameDefaultRuleSpecBuilder is a rule spec builder.
145152
BreakingFieldSameDefaultRuleSpecBuilder = &bufcheckserverutil.RuleSpecBuilder{
146153
ID: "FIELD_SAME_DEFAULT",

private/bufpkg/bufcheck/bufcheckserver/internal/bufcheckserverhandle/breaking.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,45 @@ func handleBreakingFieldSameJavaUTF8Validation(
638638
return nil
639639
}
640640

641+
// HandleBreakingFieldSameGoStripEnumPrefix is a check function.
642+
var HandleBreakingFieldSameGoStripEnumPrefix = bufcheckserverutil.NewBreakingEnumPairRuleHandler(handleBreakingFieldSameGoStripEnumPrefix)
643+
644+
func handleBreakingFieldSameGoStripEnumPrefix(
645+
responseWriter bufcheckserverutil.ResponseWriter,
646+
request bufcheckserverutil.Request,
647+
enum bufprotosource.Enum,
648+
previousEnum bufprotosource.Enum,
649+
) error {
650+
previousDescriptor, err := previousEnum.AsDescriptor()
651+
if err != nil {
652+
return err
653+
}
654+
descriptor, err := enum.AsDescriptor()
655+
if err != nil {
656+
return err
657+
}
658+
previousPrefix, err := enumGoStripEnumPrefix(previousDescriptor)
659+
if err != nil {
660+
return err
661+
}
662+
prefix, err := enumGoStripEnumPrefix(descriptor)
663+
if err != nil {
664+
return err
665+
}
666+
if previousPrefix != prefix {
667+
responseWriter.AddProtosourceAnnotationf(
668+
enumGoStripEnumPrefixLocation(enum),
669+
enumGoStripEnumPrefixLocation(previousEnum),
670+
enum.File().Path(),
671+
`%s changed Go strip enum prefix from %q to %q.`,
672+
enum.Name(),
673+
previousPrefix,
674+
prefix,
675+
)
676+
}
677+
return nil
678+
}
679+
641680
// HandleBreakingFieldSameJSType is a check function.
642681
var HandleBreakingFieldSameJSType = bufcheckserverutil.NewBreakingFieldPairRuleHandler(handleBreakingFieldSameJSType)
643682

private/bufpkg/bufcheck/bufcheckserver/internal/bufcheckserverhandle/breaking_util.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,17 @@ import (
2626
"github.com/bufbuild/protocompile/protoutil"
2727
"google.golang.org/protobuf/reflect/protoreflect"
2828
"google.golang.org/protobuf/types/descriptorpb"
29+
gofeaturespb "google.golang.org/protobuf/types/gofeaturespb"
2930
)
3031

3132
const (
32-
featuresFieldName = "features"
33-
featureNameUTF8Validation = "utf8_validation"
34-
featureNameJSONFormat = "json_format"
35-
cppFeatureNameStringType = "string_type"
36-
javaFeatureNameUTF8Validation = "utf8_validation"
33+
featuresFieldName = "features"
34+
featureNameUTF8Validation = "utf8_validation"
35+
featureNameJSONFormat = "json_format"
36+
cppFeatureNameStringType = "string_type"
37+
javaFeatureNameUTF8Validation = "utf8_validation"
38+
goFeatureNameStripEnumPrefix = "strip_enum_prefix"
39+
goFeatureNameAPILevel = "api_level"
3740
)
3841

3942
var (
@@ -257,6 +260,20 @@ func fieldJavaUTF8ValidationLocation(field bufprotosource.Field) bufprotosource.
257260
return getCustomFeatureLocation(field, ext, javaFeatureNameUTF8Validation)
258261
}
259262

263+
func enumGoStripEnumPrefix(enum protoreflect.EnumDescriptor) (gofeaturespb.GoFeatures_StripEnumPrefix, error) {
264+
val, err := customfeatures.ResolveGoFeatureForEnum(enum, goFeatureNameStripEnumPrefix, protoreflect.EnumKind)
265+
if err != nil {
266+
return 0, err
267+
}
268+
return gofeaturespb.GoFeatures_StripEnumPrefix(val.Enum()), nil
269+
}
270+
271+
func enumGoStripEnumPrefixLocation(enum bufprotosource.Enum) bufprotosource.Location {
272+
// For enums, we use the enum's features location
273+
// This is similar to how other enum features are handled
274+
return enum.Features().EnumTypeLocation()
275+
}
276+
260277
func getCustomFeatureLocation(field bufprotosource.Field, extension protoreflect.ExtensionTypeDescriptor, fieldName protoreflect.Name) bufprotosource.Location {
261278
if extension.Message() == nil {
262279
return nil

private/bufpkg/bufcheck/bufcheckserver/internal/bufcheckserverutil/customfeatures/customfeatures/customfeatures.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"fmt"
1919

2020
"github.com/bufbuild/buf/private/gen/proto/go/google/protobuf"
21+
gofeaturespb "google.golang.org/protobuf/types/gofeaturespb"
22+
2123
"github.com/bufbuild/protocompile/protoutil"
2224
"google.golang.org/protobuf/reflect/protoreflect"
2325
)
@@ -34,6 +36,18 @@ func ResolveJavaFeature(field protoreflect.FieldDescriptor, fieldName protorefle
3436
return resolveFeature(field, protobuf.E_Java.TypeDescriptor(), fieldName, expectedKind)
3537
}
3638

39+
// ResolveGoFeature returns a value for the given field name of the (pb.go) custom feature
40+
// for the given field.
41+
func ResolveGoFeature(field protoreflect.FieldDescriptor, fieldName protoreflect.Name, expectedKind protoreflect.Kind) (protoreflect.Value, error) {
42+
return resolveFeature(field, gofeaturespb.E_Go.TypeDescriptor(), fieldName, expectedKind)
43+
}
44+
45+
// ResolveGoFeatureForEnum returns a value for the given field name of the (pb.go) custom feature
46+
// for the given enum.
47+
func ResolveGoFeatureForEnum(enum protoreflect.EnumDescriptor, fieldName protoreflect.Name, expectedKind protoreflect.Kind) (protoreflect.Value, error) {
48+
return resolveFeatureForEnum(enum, gofeaturespb.E_Go.TypeDescriptor(), fieldName, expectedKind)
49+
}
50+
3751
func resolveFeature(
3852
field protoreflect.FieldDescriptor,
3953
extension protoreflect.ExtensionTypeDescriptor,
@@ -54,3 +68,24 @@ func resolveFeature(
5468
featureField,
5569
)
5670
}
71+
72+
func resolveFeatureForEnum(
73+
enum protoreflect.EnumDescriptor,
74+
extension protoreflect.ExtensionTypeDescriptor,
75+
fieldName protoreflect.Name,
76+
expectedKind protoreflect.Kind,
77+
) (protoreflect.Value, error) {
78+
featureField := extension.Message().Fields().ByName(fieldName)
79+
if featureField == nil {
80+
return protoreflect.Value{}, fmt.Errorf("unable to resolve field descriptor for %s.%s", extension.Message().FullName(), fieldName)
81+
}
82+
if featureField.Kind() != expectedKind || featureField.IsList() {
83+
return protoreflect.Value{}, fmt.Errorf("resolved field descriptor for %s.%s has unexpected type: expected optional %s, got %s %s",
84+
extension.Message().FullName(), fieldName, expectedKind, featureField.Cardinality(), featureField.Kind())
85+
}
86+
return protoutil.ResolveCustomFeature(
87+
enum,
88+
extension.Type(),
89+
featureField,
90+
)
91+
}

private/bufpkg/bufcheck/bufcheckserver/internal/bufcheckserverutil/customfeatures/customfeatures/customfeatures_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"testing"
1919

2020
"github.com/bufbuild/buf/private/gen/proto/go/google/protobuf"
21+
gofeaturespb "google.golang.org/protobuf/types/gofeaturespb"
2122
"github.com/stretchr/testify/require"
2223
"google.golang.org/protobuf/reflect/protoreflect"
2324
"google.golang.org/protobuf/types/descriptorpb"
@@ -40,3 +41,12 @@ func TestResolveJavaFeatures(t *testing.T) {
4041
// This will use the default value for proto2
4142
require.Equal(t, protobuf.JavaFeatures_DEFAULT.Number(), val.Enum())
4243
}
44+
45+
func TestResolveGoFeatures(t *testing.T) {
46+
t.Parallel()
47+
field := (*descriptorpb.FileDescriptorProto)(nil).ProtoReflect().Descriptor().Fields().ByName("package")
48+
val, err := ResolveGoFeature(field, "api_level", protoreflect.EnumKind)
49+
require.NoError(t, err)
50+
// This will use the default value for proto2
51+
require.Equal(t, gofeaturespb.GoFeatures_API_LEVEL_UNSPECIFIED.Number(), val.Enum())
52+
}

private/bufpkg/bufconfig/generate_managed_option.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"strings"
2222

2323
"google.golang.org/protobuf/types/descriptorpb"
24+
gofeaturespb "google.golang.org/protobuf/types/gofeaturespb"
2425
)
2526

2627
// FileOption is a file option.
@@ -67,6 +68,8 @@ const (
6768
FileOptionRubyPackageSuffix
6869
// FileOptionSwiftPrefix is the file option swift_prefix.
6970
FileOptionSwiftPrefix
71+
// FileOptionGoApiLevel is the Go API level feature (pb.go).api_level.
72+
FileOptionGoApiLevel
7073
)
7174

7275
// String implements fmt.Stringer.
@@ -120,6 +123,7 @@ var (
120123
FileOptionRubyPackage: "ruby_package",
121124
FileOptionRubyPackageSuffix: "ruby_package_suffix",
122125
FileOptionSwiftPrefix: "swift_prefix",
126+
FileOptionGoApiLevel: "features.(pb.go).api_level",
123127
}
124128
stringToFileOption = map[string]FileOption{
125129
"java_package": FileOptionJavaPackage,
@@ -141,6 +145,7 @@ var (
141145
"ruby_package": FileOptionRubyPackage,
142146
"ruby_package_suffix": FileOptionRubyPackageSuffix,
143147
"swift_prefix": FileOptionSwiftPrefix,
148+
"features.(pb.go).api_level": FileOptionGoApiLevel,
144149
}
145150
fileOptionToParseOverrideValueFunc = map[FileOption]func(any) (any, error){
146151
FileOptionJavaPackage: parseOverrideValue[string],
@@ -162,6 +167,7 @@ var (
162167
FileOptionRubyPackage: parseOverrideValue[string],
163168
FileOptionRubyPackageSuffix: parseOverrideValue[string],
164169
FileOptionSwiftPrefix: parseOverrideValue[string],
170+
FileOptionGoApiLevel: parseOverrideValueGoApiLevel,
165171
}
166172
fieldOptionToString = map[FieldOption]string{
167173
FieldOptionJSType: "jstype",
@@ -230,6 +236,18 @@ func parseOverrideValueJSType(override any) (any, error) {
230236
return descriptorpb.FieldOptions_JSType(jsTypeEnum), nil
231237
}
232238

239+
func parseOverrideValueGoApiLevel(override any) (any, error) {
240+
apiLevelName, ok := override.(string)
241+
if !ok {
242+
return nil, errors.New("must be one of API_LEVEL_UNSPECIFIED, API_OPEN, API_HYBRID, or API_OPAQUE")
243+
}
244+
apiLevelEnum, ok := gofeaturespb.GoFeatures_APILevel_value[apiLevelName]
245+
if !ok {
246+
return nil, errors.New("must be one of API_LEVEL_UNSPECIFIED, API_OPEN, API_HYBRID, or API_OPAQUE")
247+
}
248+
return gofeaturespb.GoFeatures_APILevel(apiLevelEnum), nil
249+
}
250+
233251
// If the file or field option override value is one of the supported enum types,
234252
// then we want to write out the string representation of the enum value, not
235253
// the corresponding int32.
@@ -271,6 +289,10 @@ func getOverrideValue(fileOptionName string, fieldOptionName string, value any)
271289
if optimizeModeValue, ok := value.(descriptorpb.FileOptions_OptimizeMode); ok {
272290
return optimizeModeValue.String(), nil
273291
}
292+
case FileOptionGoApiLevel:
293+
if apiLevelValue, ok := value.(gofeaturespb.GoFeatures_APILevel); ok {
294+
return apiLevelValue.String(), nil
295+
}
274296
}
275297
}
276298
if fieldOptionName != "" {

0 commit comments

Comments
 (0)