Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sourceinfo plugin #22

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ test: build $(BIN)/jv ## Run unit tests
golden: generate
rm -rf internal/testdata/pubsub
rm -rf internal/testdata/jsonschema
rm -rf internal/testdata/sourceinfo
buf build ./internal/proto -o -#format=json > ./internal/testdata/codegenrequest/input.json
go run internal/cmd/pubsub-generate-testdata/main.go internal/testdata/pubsub
go run internal/cmd/jsonschema-generate-testdata/main.go internal/testdata/jsonschema
go run internal/cmd/sourceinfo-generate-testdata/main.go internal/testdata/sourceinfo

.PHONY: build
build: generate ## Build all packages
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ Results in the following JSON Schema files:

</details>

## SourceInfo

The sourceinfo plugin provides a protoc-gen-sourceinfo command that generates an `.sourceinfo.binpb` file, for every `.proto` file.
Each generated file contains the associated serialized `google.protobuf.SourceInfo` value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no type called google.protobuf.SourceInfo. Do you mean google.protobuf.SourceCodeInfo? If so, this calls into question the naming of a lot of things here (package, plugin, etc).

Additionally, a SourceCodeInfo (if this is the intent) does not make a ton sense without an associated FileDescriptorProto - why not make the plugin provide the entire FileDescriptorProto/FileDescriptorSet?

Copy link
Contributor Author

@Alfus Alfus Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice typo find! I was following @jhump's naming scheme, I am happy to switch it to source code info every where if you think that would provide more clarity.
The normal code gen already comes packed with the Descriptors for normal reflection, this plugin provides just the part that is stripped out. @jhump's sourceinfo library nicely glues everything together at runtime, they same could be done in other languages.

These files can then be loaded directly or embedded in a binary.

The following helpers are provided for go via `import "github.com/bufbuild/protoschema-plugins/sourceinfo"`:

- `sourceinfo.GlobalFiles`: A drop-in replacement for protoregistry.GlobalFiles
- `sourceinfo.GlobalTypes`: A drop-in replacement for protogregistry.GlobalTypes
- `sourceinfo.RegisterAll(root string)`: Registers all `*.sourceinfo.binpb` files found under the root dir.
- `sourceinfo.RegisterAllFS(fsys fs.FS, root string)`: Same as above, except uses the given file system.

## Community

For help and discussion around Protobuf, best practices, and more, join us
Expand Down
4 changes: 2 additions & 2 deletions cmd/protoc-gen-jsonschema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ package main

import (
"github.com/bufbuild/protoplugin"
"github.com/bufbuild/protoschema-plugins/internal/protoschema"
"github.com/bufbuild/protoschema-plugins/internal"
"github.com/bufbuild/protoschema-plugins/internal/protoschema/plugin/pluginjsonschema"
)

func main() {
protoplugin.Main(protoplugin.HandlerFunc(pluginjsonschema.Handle), protoplugin.WithVersion(protoschema.Version()))
protoplugin.Main(protoplugin.HandlerFunc(pluginjsonschema.Handle), protoplugin.WithVersion(internal.Version()))
}
4 changes: 2 additions & 2 deletions cmd/protoc-gen-pubsub/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ package main

import (
"github.com/bufbuild/protoplugin"
"github.com/bufbuild/protoschema-plugins/internal/protoschema"
"github.com/bufbuild/protoschema-plugins/internal"
"github.com/bufbuild/protoschema-plugins/internal/protoschema/plugin/pluginpubsub"
)

func main() {
protoplugin.Main(protoplugin.HandlerFunc(pluginpubsub.Handle), protoplugin.WithVersion(protoschema.Version()))
protoplugin.Main(protoplugin.HandlerFunc(pluginpubsub.Handle), protoplugin.WithVersion(internal.Version()))
}
25 changes: 25 additions & 0 deletions cmd/protoc-gen-sourceinfo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2024 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 main

import (
"github.com/bufbuild/protoplugin"
"github.com/bufbuild/protoschema-plugins/internal"
"github.com/bufbuild/protoschema-plugins/internal/protoschema/plugin/pluginsourceinfo"
)

func main() {
protoplugin.Main(protoplugin.HandlerFunc(pluginsourceinfo.Handle), protoplugin.WithVersion(internal.Version()))
}
2 changes: 1 addition & 1 deletion internal/cmd/jsonschema-generate-testdata/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Buf Technologies, Inc.
// Copyright 2024 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.
Expand Down
91 changes: 91 additions & 0 deletions internal/cmd/sourceinfo-generate-testdata/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2024 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 main

import (
"fmt"
"os"
"path/filepath"
"strings"

_ "github.com/bufbuild/protoschema-plugins/internal/gen/proto/bufext/cel/expr/conformance/proto3"
"github.com/bufbuild/protoschema-plugins/internal/protoschema/golden"
"github.com/bufbuild/protoschema-plugins/internal/protoschema/plugin/pluginsourceinfo"
"google.golang.org/protobuf/reflect/protoreflect"
)

func main() {
if err := run(); err != nil {
if errString := err.Error(); errString != "" {
_, _ = fmt.Fprintln(os.Stderr, errString)
}
os.Exit(1)
}
}

func run() error {
if len(os.Args) != 2 {
return fmt.Errorf("usage: %s [output dir]", os.Args[0])
}
outputDir := os.Args[1]
// Make sure the directory exists
if err := os.MkdirAll(outputDir, 0755); err != nil {
return err
}

testFiles, err := golden.GetTestFiles("./internal/testdata")
if err != nil {
return err
}
// TODO: Use normal the plugin to generate golden files
includePrefixes := []string{
filepath.FromSlash("buf/protoschema/test/"),
filepath.FromSlash("bufext/cel/expr/conformance/proto3/"),
}
err = nil
testFiles.RangeFiles(func(testDesc protoreflect.FileDescriptor) bool {
if !shouldIncludeFile(testDesc, includePrefixes) {
return true
}
fileName := pluginsourceinfo.GetSourceInfoPath(testDesc)
filePath := filepath.Join(outputDir, fileName)
// Create any missing directories
if err = os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err :=, always (this err should be scoped to this if statement)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a lambda that is reporting the error to the outter scope, it cannot use :=

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it shouldn't be set inside of an if statement - that's confusing to readers. I'm actually surprised that there isn't a linter that bans this in golangci-lint

err = fmt.Errorf("failed to create directory for %s: %w", filePath, err)
return false
}
var data []byte
data, err = pluginsourceinfo.GenFileContents(testDesc)
if err != nil {
err = fmt.Errorf("failed to generate source info for %s: %w", testDesc.FullName(), err)
return false
}
if err = os.WriteFile(filePath, data, 0600); err != nil {
err = fmt.Errorf("failed to write source info to %s: %w", filePath, err)
return false
}
return true
})
return err
}

func shouldIncludeFile(fileDesc protoreflect.FileDescriptor, includePrefixes []string) bool {
for _, prefix := range includePrefixes {
if strings.HasPrefix(fileDesc.Path(), prefix) {
return true
}
}
return false
}
2 changes: 2 additions & 0 deletions internal/gen/proto/buf/protoschema/test/v1/test_cases.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions internal/proto/buf/protoschema/test/v1/test_cases.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package buf.protoschema.test.v1;
import "buf/validate/validate.proto";
import "bufext/cel/expr/conformance/proto3/test_all_types.proto";

// A message comment.
message NestedReference {
// A field comment.
bufext.cel.expr.conformance.proto3.TestAllTypes.NestedMessage nested_message = 1;
}

Expand Down
21 changes: 19 additions & 2 deletions internal/protoschema/golden/golden.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ import (
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
)

// GetTestDescriptors returns the test descriptors that were generated from the ./internal/proto
// directory.
func GetTestDescriptors(testdataPath string) ([]protoreflect.MessageDescriptor, error) {
func GetTestFileDescriptorSet(testdataPath string) (*descriptorpb.FileDescriptorSet, error) {
inputPath := filepath.Join(filepath.FromSlash(testdataPath), "codegenrequest", "input.json")
input, err := os.ReadFile(inputPath)
if err != nil {
Expand All @@ -39,9 +40,25 @@ func GetTestDescriptors(testdataPath string) ([]protoreflect.MessageDescriptor,
if err = (&protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(input, fdset); err != nil {
return nil, fmt.Errorf("failed to parse file descriptor set at %q: %w", inputPath, err)
}
return fdset, nil
}

func GetTestFiles(testdataPath string) (*protoregistry.Files, error) {
fdset, err := GetTestFileDescriptorSet(testdataPath)
if err != nil {
return nil, err
}
files, err := protodesc.NewFiles(fdset)
if err != nil {
return nil, fmt.Errorf("failed to link file descriptor set at %q: %w", inputPath, err)
return nil, fmt.Errorf("failed to link file descriptor set at %q: %w", testdataPath, err)
}
return files, nil
}

func GetTestDescriptors(testdataPath string) ([]protoreflect.MessageDescriptor, error) {
files, err := GetTestFiles(testdataPath)
if err != nil {
return nil, err
}
types := dynamicpb.NewTypes(files)

Expand Down
66 changes: 66 additions & 0 deletions internal/protoschema/plugin/pluginsourceinfo/pluginsourceinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2024 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 pluginsourceinfo

import (
"context"
"fmt"
"strings"

"github.com/bufbuild/protoplugin"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
)

const FileExtension = ".sourceinfo.binpb"

// GetSourceInfoPath returns the path to the source info file for the given file descriptor.
func GetSourceInfoPath(fileDescriptor protoreflect.FileDescriptor) string {
path := fileDescriptor.Path()
path = strings.TrimSuffix(path, ".proto")
return fmt.Sprintf("%s%s", path, FileExtension)
}

func GenFileContents(fileDescriptor protoreflect.FileDescriptor) ([]byte, error) {
// Convert the file descriptor to a descriptorpb.FileDescriptorProto.
fileDescProto := protodesc.ToFileDescriptorProto(fileDescriptor)
return proto.Marshal(fileDescProto.SourceCodeInfo)
}

// Handle implements protoplugin.Handler and is the main entry point for the plugin.
func Handle(
_ context.Context,
_ protoplugin.PluginEnv,
responseWriter protoplugin.ResponseWriter,
request protoplugin.Request,
) error {
fileDescriptors, err := request.FileDescriptorsToGenerate()
if err != nil {
return err
}
for _, fileDescriptor := range fileDescriptors {
// Write the YAML string to the response.
data, err := GenFileContents(fileDescriptor)
if err != nil {
return err
}
name := GetSourceInfoPath(fileDescriptor)
responseWriter.AddFile(name, string(data))
}

responseWriter.SetFeatureProto3Optional()
return nil
}
Loading