From 5531f1ae07a4783476771d67d3712fe50503198e Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Fri, 2 Oct 2026 13:34:48 -0500 Subject: [PATCH] Update bufplugin-go to support module name field Update bufplugin-go to the latest bufplugin module (and SDK). Surface the module name to callers and add round trip tests for the module name. --- README.md | 1 - check/check_service_handler_test.go | 51 +++++++++++ check/checktest/checktest.go | 30 ++++++- check/compare.go | 47 +++++------ check/module_name_test.go | 88 +++++++++++++++++++ check/request.go | 2 +- check/response_writer.go | 2 +- check/testdata/module_name/a.proto | 9 ++ check/testdata/module_name/b.proto | 5 ++ descriptor/compare.go | 20 ++--- descriptor/file_descriptor.go | 18 ++++ descriptor/file_descriptor_test.go | 101 ++++++++++++++++++++++ descriptor/module_name.go | 126 ++++++++++++++++++++++++++++ descriptor/module_name_test.go | 71 ++++++++++++++++ go.mod | 9 +- go.sum | 25 ++---- internal/pkg/compare/compare.go | 26 ------ 17 files changed, 536 insertions(+), 95 deletions(-) create mode 100644 check/module_name_test.go create mode 100644 check/testdata/module_name/a.proto create mode 100644 check/testdata/module_name/b.proto create mode 100644 descriptor/file_descriptor_test.go create mode 100644 descriptor/module_name.go create mode 100644 descriptor/module_name_test.go delete mode 100644 internal/pkg/compare/compare.go diff --git a/README.md b/README.md index 3d5c167..831124b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # bufplugin-go [![Build](https://github.com/bufbuild/bufplugin-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/bufbuild/bufplugin-go/actions/workflows/ci.yaml) -[![Report Card](https://goreportcard.com/badge/buf.build/go/bufplugin)](https://goreportcard.com/report/buf.build/go/bufplugin) [![GoDoc](https://pkg.go.dev/badge/buf.build/go/bufplugin.svg)](https://pkg.go.dev/buf.build/go/bufplugin) [![Slack](https://img.shields.io/badge/slack-buf-%23e01563)](https://buf.build/links/slack) diff --git a/check/check_service_handler_test.go b/check/check_service_handler_test.go index 74c04ae..eefcef7 100644 --- a/check/check_service_handler_test.go +++ b/check/check_service_handler_test.go @@ -147,3 +147,54 @@ func TestCheckServiceHandlerNoSourceCodeInfo(t *testing.T) { ) require.NoError(t, err) } + +func TestCheckServiceHandlerModuleName(t *testing.T) { + t.Parallel() + + checkServiceHandler, err := NewCheckServiceHandler( + &Spec{ + Rules: []*RuleSpec{ + testNewSimpleLintRuleSpec("RULE1", nil, true, false, nil), + }, + }, + ) + require.NoError(t, err) + + newCheckRequest := func(moduleName *descriptorv1.ModuleName) *checkv1.CheckRequest { + return checkv1.CheckRequest_builder{ + FileDescriptors: []*descriptorv1.FileDescriptor{ + descriptorv1.FileDescriptor_builder{ + FileDescriptorProto: &descriptorpb.FileDescriptorProto{ + Name: new("foo.proto"), + }, + ModuleName: moduleName, + }.Build(), + }, + }.Build() + } + + _, err = checkServiceHandler.Check( + t.Context(), + newCheckRequest( + descriptorv1.ModuleName_builder{ + Registry: "buf.build", + Owner: "acme", + Module: "weather", + }.Build(), + ), + ) + require.NoError(t, err) + + _, err = checkServiceHandler.Check( + t.Context(), + newCheckRequest( + descriptorv1.ModuleName_builder{ + Owner: "acme", + Module: "weather", + }.Build(), + ), + ) + pluginrpcError := &pluginrpc.Error{} + require.ErrorAs(t, err, &pluginrpcError) + require.Equal(t, pluginrpc.CodeInvalidArgument, pluginrpcError.Code()) +} diff --git a/check/checktest/checktest.go b/check/checktest/checktest.go index 82e0038..f2f4350 100644 --- a/check/checktest/checktest.go +++ b/check/checktest/checktest.go @@ -98,7 +98,7 @@ type RequestSpec struct { // // Required. Files *ProtoFileSpec - // AgainstFiles specifies the input against files to test against, if anoy. + // AgainstFiles specifies the input against files to test against, if any. AgainstFiles *ProtoFileSpec // RuleIDs are the specific RuleIDs to run. RuleIDs []string @@ -160,6 +160,12 @@ type ProtoFileSpec struct { // // This corresponds to arguments passed to protoc. FilePaths []string + // ModuleName is the name of the module that the FilePaths belong to, in the + // form registry/owner/module. + // + // If set, all files that are not imports will have this ModuleName. + // Optional. + ModuleName string } // ToFileDescriptors compiles the files into descriptor.FileDescriptors. @@ -172,7 +178,15 @@ func (p *ProtoFileSpec) ToFileDescriptors(ctx context.Context) ([]descriptor.Fil if err := validateProtoFileSpec(p); err != nil { return nil, err } - return compile(ctx, p.DirPaths, p.FilePaths) + var moduleName descriptor.ModuleName + if p.ModuleName != "" { + var err error + moduleName, err = descriptor.ParseModuleName(p.ModuleName) + if err != nil { + return nil, err + } + } + return compile(ctx, p.DirPaths, p.FilePaths, moduleName) } // ExpectedAnnotation contains the values expected from an Annotation. @@ -315,7 +329,12 @@ func expectedAnnotationForAnnotation(annotation check.Annotation) ExpectedAnnota return expectedAnnotation } -func compile(ctx context.Context, dirPaths []string, filePaths []string) ([]descriptor.FileDescriptor, error) { +func compile( + ctx context.Context, + dirPaths []string, + filePaths []string, + moduleName descriptor.ModuleName, +) ([]descriptor.FileDescriptor, error) { dirPaths = fromSlashPaths(dirPaths) filePaths = fromSlashPaths(filePaths) toSlashFilePathMap := make(map[string]struct{}, len(filePaths)) @@ -361,11 +380,16 @@ func compile(ctx context.Context, dirPaths []string, filePaths []string) ([]desc fileDescriptorProto, filePathToUnusedDependencyFilePaths[fileDescriptorProto.GetName()], ) + var protoModuleName *descriptorv1.ModuleName + if isNotImport && moduleName != nil { + protoModuleName = moduleName.ToProto() + } protoFileDescriptors[i] = descriptorv1.FileDescriptor_builder{ FileDescriptorProto: fileDescriptorProto, IsImport: !isNotImport, IsSyntaxUnspecified: isSyntaxUnspecified, UnusedDependency: unusedDependencyIndexes, + ModuleName: protoModuleName, }.Build() } return descriptor.FileDescriptorsForProtoFileDescriptors(protoFileDescriptors) diff --git a/check/compare.go b/check/compare.go index fc12b2d..6e4e968 100644 --- a/check/compare.go +++ b/check/compare.go @@ -22,23 +22,20 @@ import ( // CompareAnnotations returns -1 if one < two, 1 if one > two, 0 otherwise. func CompareAnnotations(one Annotation, two Annotation) int { - if one == nil && two == nil { + switch { + case one == nil && two == nil: return 0 - } - if one == nil && two != nil { + case one == nil: return -1 - } - if one != nil && two == nil { + case two == nil: return 1 } if compare := strings.Compare(one.RuleID(), two.RuleID()); compare != 0 { return compare } - if compare := descriptor.CompareFileLocations(one.FileLocation(), two.FileLocation()); compare != 0 { return compare } - if compare := descriptor.CompareFileLocations(one.AgainstFileLocation(), two.AgainstFileLocation()); compare != 0 { return compare } @@ -47,13 +44,12 @@ func CompareAnnotations(one Annotation, two Annotation) int { // CompareRules returns -1 if one < two, 1 if one > two, 0 otherwise. func CompareRules(one Rule, two Rule) int { - if one == nil && two == nil { + switch { + case one == nil && two == nil: return 0 - } - if one == nil && two != nil { + case one == nil: return -1 - } - if one != nil && two == nil { + case two == nil: return 1 } return strings.Compare(one.ID(), two.ID()) @@ -61,13 +57,12 @@ func CompareRules(one Rule, two Rule) int { // CompareCategories returns -1 if one < two, 1 if one > two, 0 otherwise. func CompareCategories(one Category, two Category) int { - if one == nil && two == nil { + switch { + case one == nil && two == nil: return 0 - } - if one == nil && two != nil { + case one == nil: return -1 - } - if one != nil && two == nil { + case two == nil: return 1 } return strings.Compare(one.ID(), two.ID()) @@ -77,13 +72,12 @@ func CompareCategories(one Category, two Category) int { // compareRuleSpecs returns -1 if one < two, 1 if one > two, 0 otherwise. func compareRuleSpecs(one *RuleSpec, two *RuleSpec) int { - if one == nil && two == nil { + switch { + case one == nil && two == nil: return 0 - } - if one == nil && two != nil { + case one == nil: return -1 - } - if one != nil && two == nil { + case two == nil: return 1 } return strings.Compare(one.ID, two.ID) @@ -91,13 +85,12 @@ func compareRuleSpecs(one *RuleSpec, two *RuleSpec) int { // compareCategorySpecs returns -1 if one < two, 1 if one > two, 0 otherwise. func compareCategorySpecs(one *CategorySpec, two *CategorySpec) int { - if one == nil && two == nil { + switch { + case one == nil && two == nil: return 0 - } - if one == nil && two != nil { + case one == nil: return -1 - } - if one != nil && two == nil { + case two == nil: return 1 } return strings.Compare(one.ID, two.ID) diff --git a/check/module_name_test.go b/check/module_name_test.go new file mode 100644 index 0000000..142801e --- /dev/null +++ b/check/module_name_test.go @@ -0,0 +1,88 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package check_test + +import ( + "context" + "testing" + + "buf.build/go/bufplugin/check" + "buf.build/go/bufplugin/check/checktest" + "github.com/stretchr/testify/require" +) + +func TestModuleName(t *testing.T) { + t.Parallel() + const ruleID = "TEST_MODULE_NAME" + spec := &check.Spec{ + Rules: []*check.RuleSpec{ + { + ID: ruleID, + Default: true, + Purpose: "Purpose.", + Type: check.RuleTypeLint, + Handler: check.RuleHandlerFunc( + func(_ context.Context, responseWriter check.ResponseWriter, request check.Request) error { + for _, fileDescriptor := range request.FileDescriptors() { + message := "" + if moduleName := fileDescriptor.ModuleName(); moduleName != nil { + message = moduleName.String() + } + responseWriter.AddAnnotation( + check.WithMessage(message), + check.WithFileName(fileDescriptor.ProtoreflectFileDescriptor().Path()), + ) + } + return nil + }, + ), + }, + }, + } + + checktest.CheckTest{ + Request: &checktest.RequestSpec{ + Files: &checktest.ProtoFileSpec{ + DirPaths: []string{"testdata/module_name"}, + FilePaths: []string{"a.proto"}, + ModuleName: "buf.build/acme/weather", + }, + }, + Spec: spec, + ExpectedAnnotations: []checktest.ExpectedAnnotation{ + { + RuleID: ruleID, + Message: "buf.build/acme/weather", + FileLocation: &checktest.ExpectedFileLocation{FileName: "a.proto", EndLine: 8, EndColumn: 1}, + }, + { + RuleID: ruleID, + Message: "", + FileLocation: &checktest.ExpectedFileLocation{FileName: "b.proto", EndLine: 4, EndColumn: 12}, + }, + }, + }.Run(t) +} + +func TestModuleNameInvalidProtoFileSpec(t *testing.T) { + t.Parallel() + + _, err := (&checktest.ProtoFileSpec{ + DirPaths: []string{"testdata/module_name"}, + FilePaths: []string{"a.proto"}, + ModuleName: "buf.build/acme", + }).ToFileDescriptors(t.Context()) + require.ErrorContains(t, err, `invalid module name "buf.build/acme"`) +} diff --git a/check/request.go b/check/request.go index afb4727..b1d71f3 100644 --- a/check/request.go +++ b/check/request.go @@ -52,7 +52,7 @@ type Request interface { // If empty, all default Rules will be used. // The returned RuleIDs will be sorted. // - // This may return more than 250 IDs; the underlying Client implemention is required to do + // This may return more than 250 IDs; the underlying Client implementation is required to do // any necessary chunking. // // RuleHandlers can safely ignore this - the handling of RuleIDs will have already diff --git a/check/response_writer.go b/check/response_writer.go index 4b2f47b..9859162 100644 --- a/check/response_writer.go +++ b/check/response_writer.go @@ -25,7 +25,7 @@ import ( var errCannotReuseResponseWriter = errors.New("cannot reuse ResponseWriter") -// ResponseWriter is used by plugin implmentations to add Annotations to responses. +// ResponseWriter is used by plugin implementations to add Annotations to responses. // // A ResponseWriter is tied to a specific rule, and is passed to a RuleHandler. // The ID of the Rule will be automatically populated for any added Annotations. diff --git a/check/testdata/module_name/a.proto b/check/testdata/module_name/a.proto new file mode 100644 index 0000000..f0e07c4 --- /dev/null +++ b/check/testdata/module_name/a.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package a; + +import "b.proto"; + +message A { + b.B b = 1; +} diff --git a/check/testdata/module_name/b.proto b/check/testdata/module_name/b.proto new file mode 100644 index 0000000..867d0a7 --- /dev/null +++ b/check/testdata/module_name/b.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +package b; + +message B {} diff --git a/descriptor/compare.go b/descriptor/compare.go index 10714bd..c2d767b 100644 --- a/descriptor/compare.go +++ b/descriptor/compare.go @@ -15,36 +15,34 @@ package descriptor import ( + "cmp" "slices" "strings" - - "buf.build/go/bufplugin/internal/pkg/compare" ) // CompareFileLocations returns -1 if one < two, 1 if one > two, 0 otherwise. func CompareFileLocations(one FileLocation, two FileLocation) int { - if one == nil && two == nil { + switch { + case one == nil && two == nil: return 0 - } - if one == nil && two != nil { + case one == nil: return -1 - } - if one != nil && two == nil { + case two == nil: return 1 } if compare := strings.Compare(one.FileDescriptor().ProtoreflectFileDescriptor().Path(), two.FileDescriptor().ProtoreflectFileDescriptor().Path()); compare != 0 { return compare } - if compare := compare.CompareInts(one.StartLine(), two.StartLine()); compare != 0 { + if compare := cmp.Compare(one.StartLine(), two.StartLine()); compare != 0 { return compare } - if compare := compare.CompareInts(one.StartColumn(), two.StartColumn()); compare != 0 { + if compare := cmp.Compare(one.StartColumn(), two.StartColumn()); compare != 0 { return compare } - if compare := compare.CompareInts(one.EndLine(), two.EndLine()); compare != 0 { + if compare := cmp.Compare(one.EndLine(), two.EndLine()); compare != 0 { return compare } - if compare := compare.CompareInts(one.EndColumn(), two.EndColumn()); compare != 0 { + if compare := cmp.Compare(one.EndColumn(), two.EndColumn()); compare != 0 { return compare } if compare := slices.Compare(one.unclonedSourcePath(), two.unclonedSourcePath()); compare != 0 { diff --git a/descriptor/file_descriptor.go b/descriptor/file_descriptor.go index 78a3986..272b87c 100644 --- a/descriptor/file_descriptor.go +++ b/descriptor/file_descriptor.go @@ -62,6 +62,11 @@ type FileDescriptor interface { // This matches the shape of the PublicDependency and WeakDependency fields. UnusedDependencyIndexes() []int32 + // ModuleName returns the name of the module that the File belongs to. + // + // Returns nil if the File does not belong to a module, or if the module name is not known. + ModuleName() ModuleName + // ToProto converts the FileDescriptor to its Protobuf representation. ToProto() *descriptorv1.FileDescriptor @@ -113,6 +118,7 @@ func FileDescriptorsForProtoFileDescriptors(protoFileDescriptors []*descriptorv1 protoFileDescriptor.GetIsImport(), protoFileDescriptor.GetIsSyntaxUnspecified(), protoFileDescriptor.GetUnusedDependency(), + ModuleNameForProtoModuleName(protoFileDescriptor.GetModuleName()), ), ) return true @@ -137,6 +143,7 @@ type fileDescriptor struct { isImport bool isSyntaxUnspecified bool unusedDependencyIndexes []int32 + moduleName ModuleName } func newFileDescriptor( @@ -145,6 +152,7 @@ func newFileDescriptor( isImport bool, isSyntaxUnspecified bool, unusedDependencyIndexes []int32, + moduleName ModuleName, ) *fileDescriptor { return &fileDescriptor{ protoreflectFileDescriptor: protoreflectFileDescriptor, @@ -152,6 +160,7 @@ func newFileDescriptor( isImport: isImport, isSyntaxUnspecified: isSyntaxUnspecified, unusedDependencyIndexes: unusedDependencyIndexes, + moduleName: moduleName, } } @@ -175,15 +184,24 @@ func (f *fileDescriptor) UnusedDependencyIndexes() []int32 { return slices.Clone(f.unusedDependencyIndexes) } +func (f *fileDescriptor) ModuleName() ModuleName { + return f.moduleName +} + func (f *fileDescriptor) ToProto() *descriptorv1.FileDescriptor { if f == nil { return nil } + var protoModuleName *descriptorv1.ModuleName + if f.moduleName != nil { + protoModuleName = f.moduleName.ToProto() + } return descriptorv1.FileDescriptor_builder{ FileDescriptorProto: f.fileDescriptorProto, IsImport: f.isImport, IsSyntaxUnspecified: f.isSyntaxUnspecified, UnusedDependency: f.unusedDependencyIndexes, + ModuleName: protoModuleName, }.Build() } diff --git a/descriptor/file_descriptor_test.go b/descriptor/file_descriptor_test.go new file mode 100644 index 0000000..f0cbfd0 --- /dev/null +++ b/descriptor/file_descriptor_test.go @@ -0,0 +1,101 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package descriptor + +import ( + "testing" + + descriptorv1 "buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go/buf/plugin/descriptor/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" +) + +func TestFileDescriptorRoundTrip(t *testing.T) { + t.Parallel() + + protoFileDescriptors := []*descriptorv1.FileDescriptor{ + descriptorv1.FileDescriptor_builder{ + FileDescriptorProto: &descriptorpb.FileDescriptorProto{ + Name: new("a.proto"), + Syntax: new("proto3"), + Dependency: []string{"b.proto", "c.proto"}, + }, + UnusedDependency: []int32{1}, + ModuleName: descriptorv1.ModuleName_builder{ + Registry: "buf.build", + Owner: "acme", + Module: "weather", + }.Build(), + }.Build(), + descriptorv1.FileDescriptor_builder{ + FileDescriptorProto: &descriptorpb.FileDescriptorProto{ + Name: new("b.proto"), + Syntax: new("proto3"), + }, + IsImport: true, + }.Build(), + descriptorv1.FileDescriptor_builder{ + FileDescriptorProto: &descriptorpb.FileDescriptorProto{ + Name: new("c.proto"), + }, + IsImport: true, + IsSyntaxUnspecified: true, + }.Build(), + } + fileDescriptors, err := FileDescriptorsForProtoFileDescriptors(protoFileDescriptors) + require.NoError(t, err) + require.Len(t, fileDescriptors, len(protoFileDescriptors)) + + fileNameToFileDescriptor := make(map[string]FileDescriptor, len(fileDescriptors)) + for _, fileDescriptor := range fileDescriptors { + fileNameToFileDescriptor[fileDescriptor.ProtoreflectFileDescriptor().Path()] = fileDescriptor + } + + aFileDescriptor := fileNameToFileDescriptor["a.proto"] + require.NotNil(t, aFileDescriptor) + assert.False(t, aFileDescriptor.IsImport()) + assert.False(t, aFileDescriptor.IsSyntaxUnspecified()) + assert.Equal(t, []int32{1}, aFileDescriptor.UnusedDependencyIndexes()) + require.NotNil(t, aFileDescriptor.ModuleName()) + assert.Equal(t, "buf.build/acme/weather", aFileDescriptor.ModuleName().String()) + + bFileDescriptor := fileNameToFileDescriptor["b.proto"] + require.NotNil(t, bFileDescriptor) + assert.True(t, bFileDescriptor.IsImport()) + assert.False(t, bFileDescriptor.IsSyntaxUnspecified()) + assert.Empty(t, bFileDescriptor.UnusedDependencyIndexes()) + assert.Nil(t, bFileDescriptor.ModuleName()) + + cFileDescriptor := fileNameToFileDescriptor["c.proto"] + require.NotNil(t, cFileDescriptor) + assert.True(t, cFileDescriptor.IsImport()) + assert.True(t, cFileDescriptor.IsSyntaxUnspecified()) + assert.Empty(t, cFileDescriptor.UnusedDependencyIndexes()) + assert.Nil(t, cFileDescriptor.ModuleName()) + + for _, protoFileDescriptor := range protoFileDescriptors { + fileName := protoFileDescriptor.GetFileDescriptorProto().GetName() + fileDescriptor := fileNameToFileDescriptor[fileName] + assert.Same(t, protoFileDescriptor.GetFileDescriptorProto(), fileDescriptor.FileDescriptorProto()) + assert.True( + t, + proto.Equal(protoFileDescriptor, fileDescriptor.ToProto()), + "round trip mismatch for %s", + fileName, + ) + } +} diff --git a/descriptor/module_name.go b/descriptor/module_name.go new file mode 100644 index 0000000..9bf414c --- /dev/null +++ b/descriptor/module_name.go @@ -0,0 +1,126 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package descriptor + +import ( + "fmt" + "strings" + + descriptorv1 "buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go/buf/plugin/descriptor/v1" + "buf.build/go/protovalidate" +) + +// ModuleName is the fully-qualified name of a module on the Buf Schema Registry. +type ModuleName interface { + // Registry returns the registry hostname of the module. + // + // Always present. + Registry() string + // Owner returns the name of the owner of the module, either a user or organization. + // + // Always present. + Owner() string + // Module returns the name of the module. + // + // Always present. + Module() string + // String returns the ModuleName in the form registry/owner/module. + String() string + // ToProto converts the ModuleName to its Protobuf representation. + ToProto() *descriptorv1.ModuleName + + isModuleName() +} + +// NewModuleName returns a new ModuleName. +func NewModuleName(registry string, owner string, module string) (ModuleName, error) { + moduleName := newModuleName(registry, owner, module) + if err := protovalidate.Validate(moduleName.ToProto()); err != nil { + return nil, fmt.Errorf("invalid module name %q: %w", moduleName.String(), err) + } + return moduleName, nil +} + +// ParseModuleName parses a ModuleName from a string in the form registry/owner/module. +func ParseModuleName(moduleNameString string) (ModuleName, error) { + registry, ownerAndModule, ok := strings.Cut(moduleNameString, "/") + if !ok { + return nil, fmt.Errorf("invalid module name %q: must be in the form registry/owner/module", moduleNameString) + } + owner, module, ok := strings.Cut(ownerAndModule, "/") + if !ok || strings.Contains(module, "/") { + return nil, fmt.Errorf("invalid module name %q: must be in the form registry/owner/module", moduleNameString) + } + return NewModuleName(registry, owner, module) +} + +// ModuleNameForProtoModuleName returns a new ModuleName for the given descriptorv1.ModuleName. +// +// Returns nil if protoModuleName is nil. +func ModuleNameForProtoModuleName(protoModuleName *descriptorv1.ModuleName) ModuleName { + if protoModuleName == nil { + return nil + } + return newModuleName( + protoModuleName.GetRegistry(), + protoModuleName.GetOwner(), + protoModuleName.GetModule(), + ) +} + +// *** PRIVATE *** + +type moduleName struct { + registry string + owner string + module string +} + +func newModuleName(registry string, owner string, module string) *moduleName { + return &moduleName{ + registry: registry, + owner: owner, + module: module, + } +} + +func (m *moduleName) Registry() string { + return m.registry +} + +func (m *moduleName) Owner() string { + return m.owner +} + +func (m *moduleName) Module() string { + return m.module +} + +func (m *moduleName) String() string { + return m.registry + "/" + m.owner + "/" + m.module +} + +func (m *moduleName) ToProto() *descriptorv1.ModuleName { + if m == nil { + return nil + } + return descriptorv1.ModuleName_builder{ + Registry: m.registry, + Owner: m.owner, + Module: m.module, + }.Build() +} + +func (*moduleName) isModuleName() {} diff --git a/descriptor/module_name_test.go b/descriptor/module_name_test.go new file mode 100644 index 0000000..20d56e7 --- /dev/null +++ b/descriptor/module_name_test.go @@ -0,0 +1,71 @@ +// Copyright 2024-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package descriptor + +import ( + "strings" + "testing" + + descriptorv1 "buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go/buf/plugin/descriptor/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestParseModuleName(t *testing.T) { + t.Parallel() + + moduleName, err := ParseModuleName("buf.build/acme/weather") + require.NoError(t, err) + assert.Equal(t, "buf.build", moduleName.Registry()) + assert.Equal(t, "acme", moduleName.Owner()) + assert.Equal(t, "weather", moduleName.Module()) + assert.Equal(t, "buf.build/acme/weather", moduleName.String()) + + for _, invalid := range []string{ + "", + "buf.build", + "buf.build/acme", + "buf.build/acme/weather/extra", + "/acme/weather", + "buf.build//weather", + "buf.build/acme/", + "buf.build/acme/w", + "buf.build/" + strings.Repeat("a", 49) + "/weather", + "buf.build/acme/" + strings.Repeat("a", 101), + } { + t.Run(invalid, func(t *testing.T) { + t.Parallel() + _, err := ParseModuleName(invalid) + assert.Error(t, err) + }) + } +} + +func TestModuleNameForProtoModuleName(t *testing.T) { + t.Parallel() + + assert.Nil(t, ModuleNameForProtoModuleName(nil)) + + protoModuleName := descriptorv1.ModuleName_builder{ + Registry: "buf.build", + Owner: "acme", + Module: "weather", + }.Build() + moduleName := ModuleNameForProtoModuleName(protoModuleName) + require.NotNil(t, moduleName) + assert.Equal(t, "buf.build/acme/weather", moduleName.String()) + assert.True(t, proto.Equal(protoModuleName, moduleName.ToProto())) +} diff --git a/go.mod b/go.mod index 9b13866..3c5c5c6 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module buf.build/go/bufplugin go 1.26.0 require ( - buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.1 + buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20261002170247-538130002972.2 buf.build/go/protovalidate v1.1.3 buf.build/go/spdx v0.2.0 github.com/bufbuild/protocompile v0.14.1 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 google.golang.org/protobuf v1.36.12 pluginrpc.com/pluginrpc v0.5.1-0.20260825152034-473fc805d0d1 ) @@ -17,17 +17,14 @@ require ( buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.1 // indirect cel.dev/expr v0.25.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/cel-go v0.29.0 // indirect github.com/mattn/go-isatty v0.0.24 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.28.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a9bdae1..292c8af 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.1 h1:ngSJzfqK0bvnKaVhHZ58DTt6nrBiD1IdHDMHAsQQrgM= -buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.1/go.mod h1:or0FXsDqcJfmWGc8Xx6cr8iAySi3CZ4o0K1+C39pTts= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20261002170247-538130002972.2 h1:nNNsSGs3l9F8q4NfHaIa8RdGfucw957dlFuNf9Z3H/w= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20261002170247-538130002972.2/go.mod h1:Owcuj2phVyvThVYCD74iSV8ki0dAXw/uxkdcdZScdCc= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.1 h1:Slv0uGxx219srASyiaI5C9cDlyG8kNDcXpTSYcuAeE4= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.1/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.1 h1:iXXTQ60veYbSOKfSQHT9Q3TfZxasAgTJcbo2fnNCApE= @@ -16,28 +16,20 @@ github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/cel-go v0.29.0 h1:fEG+Ja3YRwNOqnQxTyJwoByAUAvTuxUGiro/jhrm4F4= github.com/google/cel-go v0.29.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= @@ -52,10 +44,5 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= pluginrpc.com/pluginrpc v0.5.1-0.20260825152034-473fc805d0d1 h1:ijwXS090rZ+N13Rpwwz5Ku/+49aNUE73Jof1Pw/fCaw= pluginrpc.com/pluginrpc v0.5.1-0.20260825152034-473fc805d0d1/go.mod h1:ro2Em10qmKefBGOLc9HMAjRlJwJpfBS33pqfLtLj+JI= diff --git a/internal/pkg/compare/compare.go b/internal/pkg/compare/compare.go deleted file mode 100644 index eb49bf2..0000000 --- a/internal/pkg/compare/compare.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2024-2025 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package compare - -// CompareInts returns -1 if one < two, 1 if one > two, 0 otherwise. -func CompareInts(one int, two int) int { - if one < two { - return -1 - } - if one > two { - return 1 - } - return 0 -}