Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
15 changes: 13 additions & 2 deletions cmd/buf/buf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4335,12 +4335,23 @@ func TestFormatInvalidIncludePackageFiles(t *testing.T) {
func TestFormatInvalidInputDoesNotCreateDirectory(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
// The experimental parser emits multiple location-tagged diagnostics
// for a single syntax error and they get rendered through
// bufanalysis.FileAnnotationSet, which sorts and deduplicates them.
expectedStderr := strings.Join(
[]string{
filepath.FromSlash("Failure: testdata/format/invalid/invalid.proto:4:12:unexpected `.` in identifier"),
filepath.FromSlash("testdata/format/invalid/invalid.proto:4:13:unexpected tokens after `.`"),
filepath.FromSlash("testdata/format/invalid/invalid.proto:4:16:unexpected `{...}` after qualified name"),
},
"\n",
)
testRunStdoutStderrNoWarn(
t,
nil,
1,
"",
filepath.FromSlash(`Failure: testdata/format/invalid/invalid.proto:4:12: syntax error: unexpected '.', expecting '{'`),
expectedStderr,
"format",
filepath.Join("testdata", "format", "invalid"),
"-o",
Expand All @@ -4353,7 +4364,7 @@ func TestFormatInvalidInputDoesNotCreateDirectory(t *testing.T) {
nil,
1,
"",
filepath.FromSlash(`Failure: testdata/format/invalid/invalid.proto:4:12: syntax error: unexpected '.', expecting '{'`),
expectedStderr,
"format",
filepath.Join("testdata", "format", "invalid"),
"-o",
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
connectrpc.com/connect v1.19.2
connectrpc.com/grpcreflect v1.3.0
connectrpc.com/otelconnect v0.9.0
github.com/bufbuild/protocompile v0.14.2-0.20260512224539-f65c2e4a937a
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b
github.com/cli/browser v1.3.0
github.com/gofrs/flock v0.13.0
Expand Down Expand Up @@ -96,8 +96,8 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
github.com/bufbuild/protocompile v0.14.2-0.20260512224539-f65c2e4a937a h1:G3LfBC92pYwa+TFAftHlLfKUn11YtLsasd6ImYRfJHc=
github.com/bufbuild/protocompile v0.14.2-0.20260512224539-f65c2e4a937a/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E=
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 h1:f4T4k/41jHHhp2Otl6ZShDedr4wF9b+NdqIfLezx4R4=
github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E=
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ=
github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand Down Expand Up @@ -190,12 +190,12 @@ golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
Expand Down
131 changes: 87 additions & 44 deletions private/buf/bufformat/bufformat.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ import (
"io"
"sync/atomic"

"github.com/bufbuild/buf/private/bufpkg/bufanalysis"
"github.com/bufbuild/buf/private/bufpkg/bufmodule"
"github.com/bufbuild/buf/private/pkg/storage"
"github.com/bufbuild/buf/private/pkg/storage/storagemem"
"github.com/bufbuild/buf/private/pkg/thread"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/parser"
"github.com/bufbuild/protocompile/reporter"
"github.com/bufbuild/protocompile/experimental/ast"
"github.com/bufbuild/protocompile/experimental/ast/printer"
"github.com/bufbuild/protocompile/experimental/parser"
"github.com/bufbuild/protocompile/experimental/report"
"github.com/bufbuild/protocompile/experimental/seq"
"github.com/bufbuild/protocompile/experimental/source"
"github.com/bufbuild/protocompile/experimental/source/length"
)

// FormatOption is an option for formatting.
Expand Down Expand Up @@ -67,6 +72,10 @@ func FormatBucket(ctx context.Context, bucket storage.ReadBucket, opts ...Format
for _, opt := range opts {
opt(options)
}
var matcher *fullNameMatcher
if len(options.deprecatePrefixes) > 0 {
matcher = newFullNameMatcher(options.deprecatePrefixes...)
}
readWriteBucket := storagemem.NewReadWriteBucket()
paths, err := storage.AllPaths(ctx, storage.FilterReadBucket(bucket, storage.MatchPathExt(".proto")), "")
if err != nil {
Expand All @@ -84,76 +93,110 @@ func FormatBucket(ctx context.Context, bucket storage.ReadBucket, opts ...Format
defer func() {
retErr = errors.Join(retErr, readObjectCloser.Close())
}()
fileNode, err := parser.Parse(readObjectCloser.ExternalPath(), readObjectCloser, reporter.NewHandler(nil))
data, err := io.ReadAll(readObjectCloser)
if err != nil {
return err
}
file, err := parseFile(readObjectCloser, data)
if err != nil {
return err
}
if matcher != nil && applyDeprecations(file, matcher) {
deprecationMatched.Store(true)
}
writeObjectCloser, err := readWriteBucket.Put(ctx, path)
if err != nil {
return err
}
defer func() {
retErr = errors.Join(retErr, writeObjectCloser.Close())
}()
matched, err := formatFileNodeWithMatch(writeObjectCloser, fileNode, options)
if err != nil {
if err := FormatFile(writeObjectCloser, file); err != nil {
return err
}
if matched {
deprecationMatched.Store(true)
}
return writeObjectCloser.SetExternalPath(readObjectCloser.ExternalPath())
}
}
if err := thread.Parallelize(ctx, jobs); err != nil {
return nil, err
}
// If deprecation was requested but nothing matched, return an error.
if len(options.deprecatePrefixes) > 0 && !deprecationMatched.Load() {
if matcher != nil && !deprecationMatched.Load() {
return nil, fmt.Errorf("no types matched the specified deprecation prefixes")
}
return readWriteBucket, nil
}

// FormatFileNode formats the given file node and writes the result to dest.
func FormatFileNode(dest io.Writer, fileNode *ast.FileNode) error {
return formatFileNode(dest, fileNode, &formatOptions{})
// FormatFile formats the given file and writes the result to dest.
func FormatFile(dest io.Writer, file *ast.File) error {
out, err := printer.PrintFile(printer.Options{
Format: true,
Formatting: printer.Legacy(),
}, file)
if err != nil {
return err
}
_, err = io.WriteString(dest, out)
return err
}

// formatFileNode formats the given file node with options and writes the result to dest.
func formatFileNode(dest io.Writer, fileNode *ast.FileNode, options *formatOptions) error {
_, err := formatFileNodeWithMatch(dest, fileNode, options)
return err
// parseFile parses a .proto source file using the experimental parser.
//
// The parser may emit error-level diagnostics that are recoverable for
// formatting — e.g. edition 2024 import-ordering rule violations that
// canonicalization fixes anyway. We only fail when the parser produced
// no file at all, or when any top-level declaration is marked corrupt
// (signalling a syntactic failure that the formatter cannot recover
// from). This mirrors the legacy formatter's behavior of swallowing
// edition-2024-related errors while still failing on broken syntax.
func parseFile(fileInfo bufanalysis.FileInfo, data []byte) (*ast.File, error) {
// Suppress non-error diagnostics at the source. We only ever surface
// error-level diagnostics from this path.
r := &report.Report{Options: report.Options{SuppressWarnings: true}}
path := fileInfo.ExternalPath()
file, _ := parser.Parse(path, source.NewFile(path, string(data)), r)
if file == nil {
return nil, fmt.Errorf("%s: parse failed", path)
}
for decl := range seq.Values(file.Decls()) {
if def := decl.AsDef(); !def.IsZero() && def.IsCorrupt() {
return nil, parseDiagnosticsAnnotationSet(fileInfo, r)
}
}
return file, nil
}

// formatFileNodeWithMatch formats the given file node and returns whether any deprecation prefix matched.
func formatFileNodeWithMatch(dest io.Writer, fileNode *ast.FileNode, options *formatOptions) (bool, error) {
// Construct the file descriptor to ensure the AST is valid. The
// reporter swallows the known edition 2024 unsupported error (the
// parser handles it but ResultFromAST does not yet) and propagates
// all other errors. The error is identified by its span matching
// the edition value node.
errReporter := reporter.NewReporter(
func(err reporter.ErrorWithPos) error {
if fileNode.Edition == nil || fileNode.Edition.Edition.AsString() != "2024" {
return err
}
editionValueSpan := fileNode.NodeInfo(fileNode.Edition.Edition)
if err.Start() == editionValueSpan.Start() && err.End() == editionValueSpan.End() {
return nil
}
return err
},
nil,
)
if _, err := parser.ResultFromAST(fileNode, true, reporter.NewHandler(errReporter)); err != nil {
if !errors.Is(err, reporter.ErrInvalidSource) {
return false, err
// parseDiagnosticsAnnotationSet converts the error-level diagnostics into a
// file annotation set for rendering.
func parseDiagnosticsAnnotationSet(fileInfo bufanalysis.FileInfo, r *report.Report) error {
var annotations []bufanalysis.FileAnnotation
for _, diagnostic := range r.Diagnostics {
primary := diagnostic.Primary()
if primary.IsZero() {
// Spanless diagnostics (e.g. companions to fatal file-open
// errors) have no location to render and would be displayed
// as "<input>:1:1:..."; skip them. Matches build_image.go.
continue
}
start := primary.Location(primary.Start, length.Bytes)
end := primary.Location(primary.End, length.Bytes)
annotations = append(
annotations,
bufanalysis.NewFileAnnotation(
fileInfo,
start.Line,
start.Column,
end.Line,
end.Column,
"COMPILE",
diagnostic.Message(),
"", // pluginName
"", // policyName
),
)
}
formatter := newFormatter(dest, fileNode, options)
if err := formatter.Run(); err != nil {
return false, err
if len(annotations) == 0 {
return fmt.Errorf("%s: parse failed", fileInfo.ExternalPath())
}
return formatter.deprecationMatched, nil
return bufanalysis.NewFileAnnotationSet(annotations...)
}
Loading
Loading