diff --git a/cmd/buf/buf_test.go b/cmd/buf/buf_test.go
index 6f3c7e0ecd..54632a8d0b 100644
--- a/cmd/buf/buf_test.go
+++ b/cmd/buf/buf_test.go
@@ -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",
@@ -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",
diff --git a/private/buf/bufformat/bufformat.go b/private/buf/bufformat/bufformat.go
index cc8a620181..21ff184831 100644
--- a/private/buf/bufformat/bufformat.go
+++ b/private/buf/bufformat/bufformat.go
@@ -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.
@@ -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 {
@@ -84,10 +93,17 @@ 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
@@ -95,13 +111,9 @@ func FormatBucket(ctx context.Context, bucket storage.ReadBucket, opts ...Format
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())
}
}
@@ -109,51 +121,82 @@ func FormatBucket(ctx context.Context, bucket storage.ReadBucket, opts ...Format
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 ":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...)
}
diff --git a/private/buf/bufformat/deprecate.go b/private/buf/bufformat/deprecate.go
index 8b93d8799c..7e5afb6ad9 100644
--- a/private/buf/bufformat/deprecate.go
+++ b/private/buf/bufformat/deprecate.go
@@ -18,7 +18,10 @@ import (
"slices"
"strings"
- "github.com/bufbuild/protocompile/ast"
+ "github.com/bufbuild/protocompile/experimental/ast"
+ "github.com/bufbuild/protocompile/experimental/seq"
+ "github.com/bufbuild/protocompile/experimental/token"
+ "github.com/bufbuild/protocompile/experimental/token/keyword"
)
// fullNameMatcher determines which types should have deprecated options added.
@@ -31,9 +34,10 @@ func newFullNameMatcher(fqnPrefixes ...string) *fullNameMatcher {
return &fullNameMatcher{prefixes: fqnPrefixes}
}
-// matchesPrefix returns true if the given FQN matches using prefix matching.
-func (d *fullNameMatcher) matchesPrefix(fqn string) bool {
- for _, prefix := range d.prefixes {
+// matchesPrefix returns true if the given FQN matches any prefix using
+// component-aware prefix matching.
+func (m *fullNameMatcher) matchesPrefix(fqn string) bool {
+ for _, prefix := range m.prefixes {
if fqnMatchesPrefix(fqn, prefix) {
return true
}
@@ -41,12 +45,14 @@ func (d *fullNameMatcher) matchesPrefix(fqn string) bool {
return false
}
-// matchesExact returns true if the given FQN matches exactly.
-func (d *fullNameMatcher) matchesExact(fqn string) bool {
- return slices.Contains(d.prefixes, fqn)
+// matchesExact returns true if the given FQN matches any prefix exactly.
+func (m *fullNameMatcher) matchesExact(fqn string) bool {
+ return slices.Contains(m.prefixes, fqn)
}
-// fqnMatchesPrefix returns true if fqn starts with prefix using component-based matching.
+// fqnMatchesPrefix returns true if fqn equals prefix or starts with
+// "prefix.". This is component-aware matching: "foo.bar" matches
+// "foo.bar.baz" but not "foo.bart".
func fqnMatchesPrefix(fqn, prefix string) bool {
if len(prefix) > len(fqn) {
return false
@@ -57,70 +63,301 @@ func fqnMatchesPrefix(fqn, prefix string) bool {
return prefix == "" || strings.HasPrefix(fqn, prefix+".")
}
-// hasDeprecatedOption checks if a slice of declarations contains a deprecated = true option.
-func hasDeprecatedOption[T any](decls []T) bool {
- for _, decl := range decls {
- if opt, ok := any(decl).(*ast.OptionNode); ok && isDeprecatedOptionNode(opt) {
- return true
+// applyDeprecations walks the file's AST, adding deprecation markers to every
+// declaration whose fully-qualified name matches the matcher. It returns true
+// if any prefix matched a declaration, regardless of whether a new option was
+// added — already-deprecated types still count as matched, so callers can
+// distinguish a no-op pass from one that found no matches at all.
+//
+// The AST is mutated in place. After calling, the file must be re-rendered
+// via the printer to materialize the changes.
+//
+// NOTE: this function performs all mutations directly through the
+// experimental/ast, experimental/token, and experimental/seq APIs rather
+// than experimental/ast/edit. The edit package's KindAdd only appends to
+// the end of a target's decl list and cannot express the positional
+// inserts (body deprecation must precede other decls), compact-options
+// entries ([deprecated = true] on a field/enum value), or RPC-method-
+// body synthesis (turning `rpc Foo() returns (Bar);` into a body) that
+// we need. Once the edit package grows these capabilities, this file
+// should collapse into a list of edit.Edits.
+func applyDeprecations(file *ast.File, matcher *fullNameMatcher) bool {
+ if matcher == nil || len(matcher.prefixes) == 0 {
+ return false
+ }
+ var matched bool
+ pkg := packageFQN(file)
+ if pkg != "" && matcher.matchesPrefix(pkg) {
+ matched = true
+ if !hasDeprecatedDecl(file.Decls()) {
+ insertFileDeprecatedOption(file)
}
}
- return false
+ walkDecls(file, file.Decls(), pkg, matcher, &matched)
+ return matched
}
-// hasCompactDeprecatedOption checks if a CompactOptionsNode contains deprecated = true.
-func hasCompactDeprecatedOption(opts *ast.CompactOptionsNode) bool {
- if opts == nil {
- return false
+// walkDecls recursively visits the declarations under parentFQN, applying
+// deprecation mutations to every matching def.
+func walkDecls(
+ file *ast.File,
+ decls seq.Inserter[ast.DeclAny],
+ parentFQN string,
+ matcher *fullNameMatcher,
+ matched *bool,
+) {
+ for decl := range seq.Values(decls) {
+ def := decl.AsDef()
+ if def.IsZero() {
+ continue
+ }
+ name := defName(def)
+ fqn := joinFQN(parentFQN, name)
+
+ switch def.Classify() {
+ case ast.DefKindMessage, ast.DefKindService:
+ if name != "" && matcher.matchesPrefix(fqn) {
+ *matched = true
+ if !hasDeprecatedDecl(def.Body().Decls()) {
+ insertBodyDeprecatedOption(file, def.Body())
+ }
+ }
+ if !def.Body().IsZero() {
+ walkDecls(file, def.Body().Decls(), fqn, matcher, matched)
+ }
+ case ast.DefKindEnum:
+ if name != "" && matcher.matchesPrefix(fqn) {
+ *matched = true
+ if !hasDeprecatedDecl(def.Body().Decls()) {
+ insertBodyDeprecatedOption(file, def.Body())
+ }
+ }
+ // Enum values are scoped under the enum's parent, not the enum
+ // itself, so we recurse with parentFQN unchanged.
+ if !def.Body().IsZero() {
+ walkDecls(file, def.Body().Decls(), parentFQN, matcher, matched)
+ }
+ case ast.DefKindMethod:
+ if name != "" && matcher.matchesPrefix(fqn) {
+ *matched = true
+ if def.Body().IsZero() {
+ attachMethodBodyWithDeprecated(file, def)
+ } else if !hasDeprecatedDecl(def.Body().Decls()) {
+ insertBodyDeprecatedOption(file, def.Body())
+ }
+ }
+ case ast.DefKindField, ast.DefKindGroup:
+ if name != "" && matcher.matchesExact(fqn) {
+ *matched = true
+ if !hasDeprecatedCompactOption(def.Options()) {
+ addCompactDeprecated(file, def)
+ }
+ }
+ // Groups behave like messages for nested types.
+ if def.Classify() == ast.DefKindGroup && !def.Body().IsZero() {
+ walkDecls(file, def.Body().Decls(), fqn, matcher, matched)
+ }
+ case ast.DefKindEnumValue:
+ if name != "" && matcher.matchesExact(fqn) {
+ *matched = true
+ if !hasDeprecatedCompactOption(def.Options()) {
+ addCompactDeprecated(file, def)
+ }
+ }
+ case ast.DefKindOneof, ast.DefKindExtend:
+ // Oneofs and extend blocks are containers without their own FQN
+ // participation in deprecation, but their nested decls (fields)
+ // still need to be visited under the surrounding scope.
+ if !def.Body().IsZero() {
+ walkDecls(file, def.Body().Decls(), parentFQN, matcher, matched)
+ }
+ }
}
- return slices.ContainsFunc(opts.Options, isDeprecatedOptionNode)
}
-// isDeprecatedOptionNode checks if an option node is "deprecated = true".
-func isDeprecatedOptionNode(opt *ast.OptionNode) bool {
- if opt.Name == nil || len(opt.Name.Parts) != 1 {
- return false
+// packageFQN returns the canonicalized package name from a file's
+// `package ...;` declaration, or "" if there is no package.
+func packageFQN(file *ast.File) string {
+ pkg := file.Package()
+ if pkg.IsZero() {
+ return ""
}
- part := opt.Name.Parts[0]
- if part.Name == nil {
- return false
+ return pkg.Path().Canonicalized()
+}
+
+// defName returns the identifier name of a DeclDef, or "" if the def has
+// no single-identifier name (e.g. a compound option path).
+func defName(def ast.DeclDef) string {
+ ident := def.Name().AsIdent()
+ if ident.IsZero() {
+ return ""
}
- var name string
- switch n := part.Name.(type) {
- case *ast.IdentNode:
- name = n.Val
- default:
- return false
+ return ident.Name()
+}
+
+// joinFQN concatenates parent and name with a dot, handling empty inputs.
+func joinFQN(parent, name string) string {
+ if name == "" {
+ return parent
+ }
+ if parent == "" {
+ return name
+ }
+ return parent + "." + name
+}
+
+// hasDeprecatedDecl reports whether decls already contains
+// `option deprecated = true;`.
+func hasDeprecatedDecl(decls seq.Inserter[ast.DeclAny]) bool {
+ for decl := range seq.Values(decls) {
+ def := decl.AsDef()
+ if def.IsZero() || def.Classify() != ast.DefKindOption {
+ continue
+ }
+ if def.Name().IsIdents("deprecated") && exprIsTrue(def.Value()) {
+ return true
+ }
}
- if name != "deprecated" {
+ return false
+}
+
+// hasDeprecatedCompactOption reports whether opts already contains
+// `deprecated = true`.
+func hasDeprecatedCompactOption(opts ast.CompactOptions) bool {
+ if opts.IsZero() {
return false
}
- if ident, ok := opt.Val.(*ast.IdentNode); ok {
- return ident.Val == "true"
+ for entry := range seq.Values(opts.Entries()) {
+ if entry.Path.IsIdents("deprecated") && exprIsTrue(entry.Value) {
+ return true
+ }
}
return false
}
-// packageNameToString extracts package name as a dot-separated string from an identifier node.
-func packageNameToString(name ast.IdentValueNode) string {
- switch n := name.(type) {
- case *ast.IdentNode:
- return n.Val
- case *ast.CompoundIdentNode:
- components := make([]string, len(n.Components))
- for i, comp := range n.Components {
- components[i] = comp.Val
+// exprIsTrue reports whether expr is the literal `true` identifier.
+func exprIsTrue(expr ast.ExprAny) bool {
+ path := expr.AsPath()
+ if path.IsZero() {
+ return false
+ }
+ return path.Path.IsIdents("true")
+}
+
+// insertFileDeprecatedOption inserts `option deprecated = true;` at the
+// file level, positioned after the leading syntax/package/import/option
+// runs. Required because edit.KindAdd would append to the end of the
+// file's decl list — and the file-level deprecation pattern is to place
+// the option alongside the other file-level options near the top.
+//
+// (In practice, printer.Legacy() with CanonicalizeFileOrder also moves
+// file-level options into position, but we still insert at the right
+// place so the AST itself is canonical and so non-format mode prints
+// correctly.)
+func insertFileDeprecatedOption(file *ast.File) {
+ decls := file.Decls()
+ insertPos := 0
+ for i := range decls.Len() {
+ d := decls.At(i)
+ if !d.AsSyntax().IsZero() || !d.AsPackage().IsZero() || !d.AsImport().IsZero() {
+ insertPos = i + 1
+ continue
}
- return strings.Join(components, ".")
- default:
- return ""
+ if def := d.AsDef(); !def.IsZero() && def.Classify() == ast.DefKindOption {
+ insertPos = i + 1
+ continue
+ }
+ break
+ }
+ decls.Insert(insertPos, newDeprecatedOptionDecl(file).AsAny())
+}
+
+// insertBodyDeprecatedOption inserts `option deprecated = true;` at the
+// very top of a body. The legacy formatter places injected deprecation
+// markers before any other options in the body; matching that ordering
+// keeps existing goldens stable. Body decl order is not canonicalized by
+// the printer, so positional insert is required.
+func insertBodyDeprecatedOption(file *ast.File, body ast.DeclBody) {
+ body.Decls().Insert(0, newDeprecatedOptionDecl(file).AsAny())
+}
+
+// attachMethodBodyWithDeprecated synthesizes a body containing
+// `option deprecated = true;` and attaches it to a method definition that
+// previously ended with a semicolon. The printer ignores the stored
+// semicolon when a body is present (see printer/decl.go printMethod), so
+// we do not need to clear it — and DeclDef has no public way to do so today.
+func attachMethodBodyWithDeprecated(file *ast.File, def ast.DeclDef) {
+ stream := file.Stream()
+ nodes := file.Nodes()
+ openBrace := stream.NewPunct(keyword.LBrace.String())
+ closeBrace := stream.NewPunct(keyword.RBrace.String())
+ stream.NewFused(openBrace, closeBrace)
+ body := nodes.NewDeclBody(openBrace)
+ seq.Append(body.Decls(), newDeprecatedOptionDecl(file).AsAny())
+ def.SetBody(body)
+}
+
+// addCompactDeprecated adds `deprecated = true` to a def's compact options.
+// If the def has no compact-options bracket yet, one is synthesized. The
+// new entry is prepended (index 0) to match the legacy formatter's golden
+// output, which always lists `deprecated` first when adding to a field or
+// enum value that already has other compact options.
+func addCompactDeprecated(file *ast.File, def ast.DeclDef) {
+ stream := file.Stream()
+ nodes := file.Nodes()
+ opts := def.Options()
+ if opts.IsZero() {
+ openBracket := stream.NewPunct(keyword.LBracket.String())
+ closeBracket := stream.NewPunct(keyword.RBracket.String())
+ stream.NewFused(openBracket, closeBracket)
+ opts = nodes.NewCompactOptions(openBracket)
+ def.SetOptions(opts)
}
+ entries := opts.Entries()
+ deprecatedOpt := newDeprecatedOption(file)
+ if entries.Len() == 0 {
+ seq.Append(entries, deprecatedOpt)
+ return
+ }
+ // Prepend with a comma so the new entry is followed by ", " and the
+ // previous first entry becomes the second. Commas in this model are
+ // owned by the entry they follow.
+ comma := stream.NewPunct(keyword.Comma.String())
+ entries.InsertComma(0, deprecatedOpt, comma)
+}
+
+// newDeprecatedOptionDecl synthesizes an `option deprecated = true;` decl.
+func newDeprecatedOptionDecl(file *ast.File) ast.DeclDef {
+ stream := file.Stream()
+ nodes := file.Nodes()
+ return nodes.NewDeclDef(ast.DeclDefArgs{
+ Keyword: stream.NewIdent(keyword.Option.String()),
+ Name: nodes.NewPath(
+ nodes.NewPathComponent(token.Zero, stream.NewIdent("deprecated")),
+ ),
+ Equals: stream.NewPunct(keyword.Assign.String()),
+ Value: ast.ExprPath{
+ Path: nodes.NewPath(
+ nodes.NewPathComponent(token.Zero, stream.NewIdent(keyword.True.String())),
+ ),
+ }.AsAny(),
+ Semicolon: stream.NewPunct(keyword.Semi.String()),
+ })
}
-// parentFQN returns the parent FQN by removing the last component.
-// For example, "foo.bar.baz" returns "foo.bar".
-func parentFQN(fqn string) string {
- if idx := strings.LastIndex(fqn, "."); idx >= 0 {
- return fqn[:idx]
+// newDeprecatedOption synthesizes a `deprecated = true` compact option entry.
+func newDeprecatedOption(file *ast.File) ast.Option {
+ stream := file.Stream()
+ nodes := file.Nodes()
+ return ast.Option{
+ Path: nodes.NewPath(
+ nodes.NewPathComponent(token.Zero, stream.NewIdent("deprecated")),
+ ),
+ Equals: stream.NewPunct(keyword.Assign.String()),
+ Value: ast.ExprPath{
+ Path: nodes.NewPath(
+ nodes.NewPathComponent(token.Zero, stream.NewIdent(keyword.True.String())),
+ ),
+ }.AsAny(),
}
- return ""
}
diff --git a/private/buf/bufformat/formatter.go b/private/buf/bufformat/formatter.go
deleted file mode 100644
index 8a36fa6f87..0000000000
--- a/private/buf/bufformat/formatter.go
+++ /dev/null
@@ -1,2692 +0,0 @@
-// Copyright 2020-2026 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 bufformat
-
-import (
- "errors"
- "fmt"
- "io"
- "reflect"
- "sort"
- "strings"
- "unicode"
- "unicode/utf8"
-
- "github.com/bufbuild/protocompile/ast"
-)
-
-// formatter writes an *ast.FileNode as a .proto file.
-type formatter struct {
- writer io.Writer
- fileNode *ast.FileNode
-
- // Used to adjust comments when we remove superfluous
- // separators tp canonicalize message literals
- overrideTrailingComments map[ast.Node]ast.Comments
-
- // Current level of indentation.
- indent int
- // The last character written to writer.
- lastWritten rune
-
- // The last node written. This must be updated from all functions
- // that write comments with a node. This flag informs how the next
- // node's leading comments and whitespace should be written.
- previousNode ast.Node
-
- // If true, a space will be written to the output unless the next character
- // written is a newline (don't wait errant trailing spaces).
- pendingSpace bool
- // If true, the formatter is in the middle of printing compact options.
- inCompactOptions bool
-
- // Track runes that open blocks/scopes and are expected to increase indention
- // level. For example, when runes "{" "[" "(" ")" are written, the pending
- // value is 2 (increment three times for "{" "[" "("; decrement once for ")").
- // If it's greater than zero at the end of a line, we call In() so that
- // subsequent lines are indented. If it's less than zero at the end of a line,
- // we call Out(). This minimizes the amount of explicit indent/unindent code
- // that is needed and makes it less error-prone.
- pendingIndent int
- // If true, an inline node/sequence is being written. We treat whitespace a
- // little differently for when blocks are printed inline vs. across multiple
- // lines. So this flag informs the logic that makes those whitespace decisions.
- inline bool
-
- // Records all errors that occur during the formatting process. Nearly any
- // non-nil error represents a bug in the implementation.
- err error
-
- // deprecation tracks which types should have deprecated options injected.
- deprecation *fullNameMatcher
- // packageFQN holds the current fully-qualified name.
- packageFQN string
- // injectCompactDeprecation is set when the next compact options should have
- // deprecated = true injected at the beginning.
- injectCompactDeprecation bool
- // deprecationMatched is set to true when any type matches the deprecation prefix,
- // regardless of whether it was already deprecated.
- deprecationMatched bool
-}
-
-// newFormatter returns a new formatter for the given file.
-func newFormatter(
- writer io.Writer,
- fileNode *ast.FileNode,
- options *formatOptions,
-) *formatter {
- f := &formatter{
- writer: writer,
- fileNode: fileNode,
- overrideTrailingComments: map[ast.Node]ast.Comments{},
- }
- if options != nil && len(options.deprecatePrefixes) > 0 {
- f.deprecation = newFullNameMatcher(options.deprecatePrefixes...)
- }
- return f
-}
-
-// Run runs the formatter and writes the file's content to the formatter's writer.
-func (f *formatter) Run() error {
- f.writeFile()
- return f.err
-}
-
-// P prints a line to the generated output.
-//
-// This will emit a newline and proper indentation. If you do not
-// want to emit a newline and want to write a raw string, use
-// WriteString (which P calls).
-//
-// If strings.TrimSpace(elem) is empty, no indentation is produced.
-func (f *formatter) P(elem string) {
- if len(strings.TrimSpace(elem)) > 0 {
- // We only want to write an indent if we're
- // writing a non-empty string (not just a newline).
- f.Indent(nil)
- f.WriteString(elem)
- }
- f.WriteString("\n")
-
- if f.pendingIndent > 0 {
- f.In()
- } else if f.pendingIndent < 0 {
- f.Out()
- }
- f.pendingIndent = 0
-}
-
-// Space adds a space to the generated output.
-func (f *formatter) Space() {
- f.pendingSpace = true
-}
-
-// In increases the current level of indentation.
-func (f *formatter) In() {
- f.indent++
-}
-
-// Out reduces the current level of indentation.
-func (f *formatter) Out() {
- if f.indent <= 0 {
- // Unreachable.
- f.err = errors.Join(
- f.err,
- errors.New("internal error: attempted to decrement indentation at zero"),
- )
- return
- }
- f.indent--
-}
-
-// Indent writes the number of spaces associated
-// with the current level of indentation.
-func (f *formatter) Indent(nextNode ast.Node) {
- // only indent at beginning of line
- if f.lastWritten != '\n' {
- return
- }
- indent := f.indent
- if rn, ok := nextNode.(*ast.RuneNode); ok && indent > 0 {
- if strings.ContainsRune("}])>", rn.Rune) {
- indent--
- }
- }
- f.WriteString(strings.Repeat(" ", indent))
-}
-
-// WriteString writes the given element to the generated output.
-//
-// This will not write indentation or newlines. Use P if you
-// want to emit indentation or newlines.
-func (f *formatter) WriteString(elem string) {
- if f.pendingSpace {
- f.pendingSpace = false
- first, _ := utf8.DecodeRuneInString(elem)
-
- // We don't want "dangling spaces" before certain characters:
- // newlines, commas, and semicolons. Also, when writing
- // elements inline, we don't want spaces before close parens
- // and braces. Similarly, we don't want extra/doubled spaces
- // or dangling spaces after certain characters when printing
- // inline, like open parens/braces. So only print the space
- // if the previous and next character don't match above
- // conditions.
-
- prevBlockList := "\x00 \t\n"
- nextBlockList := "\n;,"
- if f.inline {
- prevBlockList = "\x00 \t\n<[{("
- nextBlockList = "\n;,)]}>"
- }
-
- if !strings.ContainsRune(prevBlockList, f.lastWritten) &&
- !strings.ContainsRune(nextBlockList, first) {
- if _, err := f.writer.Write([]byte{' '}); err != nil {
- f.err = errors.Join(f.err, err)
- return
- }
- }
- }
- if len(elem) == 0 {
- return
- }
- f.lastWritten, _ = utf8.DecodeLastRuneInString(elem)
- if _, err := f.writer.Write([]byte(elem)); err != nil {
- f.err = errors.Join(f.err, err)
- }
-}
-
-// SetPreviousNode sets the previously written node. This should
-// be called in all of the comment writing functions.
-func (f *formatter) SetPreviousNode(node ast.Node) {
- f.previousNode = node
-}
-
-// writeFile writes the file node.
-func (f *formatter) writeFile() {
- f.writeFileHeader()
- f.writeFileTypes()
- if f.fileNode.EOF != nil {
- info := f.nodeInfo(f.fileNode.EOF)
- f.writeMultilineComments(info.LeadingComments())
- }
- if f.lastWritten != 0 && f.lastWritten != '\n' {
- // If anything was written, we always conclude with
- // a newline.
- f.P("")
- }
-}
-
-// writeFileHeader writes the header of a .proto file. This includes the syntax,
-// package, imports, and options (in that order). The imports and options are
-// sorted. All other file elements are handled by f.writeFileTypes.
-//
-// For example,
-//
-// syntax = "proto3";
-//
-// package acme.v1.weather;
-//
-// import "acme/payment/v1/payment.proto";
-// import "google/type/datetime.proto";
-//
-// option cc_enable_arenas = true;
-// option optimize_for = SPEED;
-func (f *formatter) writeFileHeader() {
- var (
- packageNode *ast.PackageNode
- importNodes []*ast.ImportNode
- optionNodes []*ast.OptionNode
- )
- for _, fileElement := range f.fileNode.Decls {
- switch node := fileElement.(type) {
- case *ast.PackageNode:
- packageNode = node
- case *ast.ImportNode:
- importNodes = append(importNodes, node)
- case *ast.OptionNode:
- optionNodes = append(optionNodes, node)
- default:
- continue
- }
- }
- // Extract package FQN for deprecation tracking
- if packageNode != nil {
- f.packageFQN = packageNameToString(packageNode.Name)
- }
- if f.fileNode.Syntax == nil && f.fileNode.Edition == nil &&
- packageNode == nil && importNodes == nil && optionNodes == nil {
- // There aren't any header values, so we can return early.
- return
- }
- // We use a sentinel value to track which node was written first (since a proto file
- // without a syntax is valid). This is then used to ensure that we do not preserve
- // unnecessary leading whitespace for the first node written.
- first := true
- editionNode := f.fileNode.Edition
- if editionNode != nil {
- f.writeEdition(editionNode)
- first = false
- }
- if syntaxNode := f.fileNode.Syntax; syntaxNode != nil && editionNode == nil {
- f.writeSyntax(syntaxNode)
- first = false
- }
- if packageNode != nil {
- f.writePackage(packageNode, first)
- first = false
- }
- sort.Slice(importNodes, func(i, j int) bool {
- iName := importNodes[i].Name.AsString()
- jName := importNodes[j].Name.AsString()
- // "import option" sorts after all other imports. Within each
- // group, sort alphabetically by name, then by modifier
- // (public > regular > weak), and finally by comment.
- iOption := isOptionImport(importNodes[i])
- jOption := isOptionImport(importNodes[j])
- if iOption != jOption {
- return !iOption
- }
- if iName != jName {
- return iName < jName
- }
- iOrder := importSortOrder(importNodes[i])
- jOrder := importSortOrder(importNodes[j])
- if iOrder != jOrder {
- return iOrder < jOrder
- }
-
- // put commented import first
- return !f.importHasComment(importNodes[j])
- })
- for i, importNode := range importNodes {
- if i == 0 && f.previousNode != nil && !f.leadingCommentsContainBlankLine(importNode) {
- f.P("")
- }
-
- // since the imports are sorted, this will skip write imports
- // if they have appear before and dont have comment
- if i > 0 && importNode.Name.AsString() == importNodes[i-1].Name.AsString() &&
- !f.importHasComment(importNode) {
- continue
- }
-
- f.writeImport(importNode, i > 0, first)
- first = false
- }
- sort.Slice(optionNodes, func(i, j int) bool {
- // The default options (e.g. cc_enable_arenas) should always
- // be sorted above custom options (which are identified by a
- // leading '(').
- left := stringForOptionName(optionNodes[i].Name)
- right := stringForOptionName(optionNodes[j].Name)
- if strings.HasPrefix(left, "(") && !strings.HasPrefix(right, "(") {
- // Prefer the default option on the right.
- return false
- }
- if !strings.HasPrefix(left, "(") && strings.HasPrefix(right, "(") {
- // Prefer the default option on the left.
- return true
- }
- // Both options are custom, so we defer to the standard sorting.
- return left < right
- })
- for i, optionNode := range optionNodes {
- if i == 0 && f.previousNode != nil && !f.leadingCommentsContainBlankLine(optionNode) {
- f.P("")
- }
- f.writeFileOption(optionNode, i > 0, first)
- first = false
- }
- // Inject file-level deprecated option if needed
- if f.shouldInjectDeprecation(f.packageFQN) && !hasDeprecatedOption(optionNodes) {
- if len(optionNodes) == 0 && f.previousNode != nil {
- f.P("")
- }
- f.writeDeprecatedOption()
- }
-}
-
-// writeFileTypes writes the types defined in a .proto file. This includes the messages, enums,
-// services, etc. All other elements are ignored since they are handled by f.writeFileHeader.
-func (f *formatter) writeFileTypes() {
- for i, fileElement := range f.fileNode.Decls {
- switch node := fileElement.(type) {
- case *ast.PackageNode, *ast.OptionNode, *ast.ImportNode, *ast.EmptyDeclNode:
- // These elements have already been written by f.writeFileHeader.
- continue
- default:
- info := f.nodeInfo(node)
- wantNewline := f.previousNode != nil && (i == 0 || info.LeadingComments().Len() > 0)
- if wantNewline && !f.leadingCommentsContainBlankLine(node) {
- f.P("")
- }
- f.writeNode(node)
- }
- }
-}
-
-// writeSyntax writes the syntax.
-//
-// For example,
-//
-// syntax = "proto3";
-func (f *formatter) writeSyntax(syntaxNode *ast.SyntaxNode) {
- // If this is the first node, we want to ignore leading whitespace, unless there are
- // leading comments.
- info := f.nodeInfo(syntaxNode)
- f.writeStart(syntaxNode.Keyword, info.LeadingComments().Len() == 0)
- f.Space()
- f.writeInline(syntaxNode.Equals)
- f.Space()
- f.writeInline(syntaxNode.Syntax)
- f.writeLineEnd(syntaxNode.Semicolon)
-}
-
-// writeEdition writes the edition.
-//
-// For example,
-//
-// edition = "2023";
-func (f *formatter) writeEdition(editionNode *ast.EditionNode) {
- // If this is the first node, we want to ignore leading whitespace, unless there are
- // leading comments.
- info := f.nodeInfo(editionNode)
- f.writeStart(editionNode.Keyword, info.LeadingComments().Len() == 0)
- f.Space()
- f.writeInline(editionNode.Equals)
- f.Space()
- f.writeInline(editionNode.Edition)
- f.writeLineEnd(editionNode.Semicolon)
-}
-
-// writePackage writes the package.
-//
-// For example,
-//
-// package acme.weather.v1;
-func (f *formatter) writePackage(packageNode *ast.PackageNode, first bool) {
- // If this is the first node, we want to ignore leading whitespace, unless there are
- // leading comments.
- info := f.nodeInfo(packageNode)
- f.writeStart(packageNode.Keyword, first && info.LeadingComments().Len() == 0)
- f.Space()
- f.writeInline(packageNode.Name)
- f.writeLineEnd(packageNode.Semicolon)
-}
-
-// writeImport writes an import statement.
-//
-// For example,
-//
-// import "google/protobuf/descriptor.proto";
-func (f *formatter) writeImport(importNode *ast.ImportNode, forceCompact, first bool) {
- // If this is the first node, we want to ignore leading whitespace, unless there are
- // leading comments.
- f.writeStartMaybeCompact(importNode.Keyword, forceCompact, first && !f.importHasComment(importNode))
- f.Space()
- if importNode.Modifier != nil {
- f.writeInline(importNode.Modifier)
- f.Space()
- }
- f.writeInline(importNode.Name)
- f.writeLineEnd(importNode.Semicolon)
-}
-
-// writeFileOption writes a file option. This function is slightly
-// different than f.writeOption because file options are sorted at
-// the top of the file, and leading comments are adjusted accordingly.
-func (f *formatter) writeFileOption(optionNode *ast.OptionNode, forceCompact, first bool) {
- // If this is the first node, we want to ignore leading whitespace, unless there are
- // leading comments.
- info := f.nodeInfo(optionNode)
- f.writeStartMaybeCompact(optionNode.Keyword, forceCompact, first && info.LeadingComments().Len() == 0)
- f.Space()
- f.writeNode(optionNode.Name)
- f.Space()
- f.writeInline(optionNode.Equals)
- if node, ok := optionNode.Val.(*ast.CompoundStringLiteralNode); ok {
- // Compound string literals are written across multiple lines
- // immediately after the '=', so we don't need a trailing
- // space in the option prefix.
- f.writeCompoundStringLiteralIndentEndInline(node)
- f.writeLineEnd(optionNode.Semicolon)
- return
- }
- f.Space()
- f.writeInline(optionNode.Val)
- f.writeLineEnd(optionNode.Semicolon)
-}
-
-// writeOption writes an option.
-//
-// For example,
-//
-// option go_package = "github.com/foo/bar";
-func (f *formatter) writeOption(optionNode *ast.OptionNode) {
- f.writeOptionPrefix(optionNode)
- if optionNode.Semicolon != nil {
- if node, ok := optionNode.Val.(*ast.CompoundStringLiteralNode); ok {
- // Compound string literals are written across multiple lines
- // immediately after the '=', so we don't need a trailing
- // space in the option prefix.
- f.writeCompoundStringLiteralIndentEndInline(node)
- f.writeLineEnd(optionNode.Semicolon)
- return
- }
- f.writeInline(optionNode.Val)
- f.writeLineEnd(optionNode.Semicolon)
- return
- }
-
- if node, ok := optionNode.Val.(*ast.CompoundStringLiteralNode); ok {
- // We write the compound string literal node and end in-line to handle any commas for
- // the option.
- f.writeCompoundStringLiteralIndentEndInline(node)
- return
- }
- f.writeInline(optionNode.Val)
-}
-
-// writeLastCompactOption writes a compact option but preserves its the
-// trailing end comments. This is only used for the last compact option
-// since it's the only time a trailing ',' will be omitted.
-//
-// For example,
-//
-// [
-// deprecated = true,
-// json_name = "something" // Trailing comment on the last element.
-// ]
-func (f *formatter) writeLastCompactOption(optionNode *ast.OptionNode) {
- f.writeOptionPrefix(optionNode)
- f.writeLineEnd(optionNode.Val)
-}
-
-// writeOptionValue writes the option prefix, which makes up all of the
-// option's definition, excluding the final token(s).
-//
-// For example,
-//
-// deprecated =
-func (f *formatter) writeOptionPrefix(optionNode *ast.OptionNode) {
- if optionNode.Keyword != nil {
- // Compact options don't have the keyword.
- f.writeStart(optionNode.Keyword, false)
- f.Space()
- f.writeNode(optionNode.Name)
- } else {
- f.writeStart(optionNode.Name, false)
- }
- f.Space()
- f.writeInline(optionNode.Equals)
- f.Space()
-}
-
-// writeOptionName writes an option name.
-//
-// For example,
-//
-// go_package
-// (custom.thing)
-// (custom.thing).bridge.(another.thing)
-func (f *formatter) writeOptionName(optionNameNode *ast.OptionNameNode) {
- for i := range optionNameNode.Parts {
- if f.inCompactOptions && i == 0 {
- // The leading comments of the first token (either open rune or the
- // name) will have already been written, so we need to handle this
- // case specially.
- fieldReferenceNode := optionNameNode.Parts[0]
- if fieldReferenceNode.Open != nil {
- f.writeNode(fieldReferenceNode.Open)
- if info := f.nodeInfo(fieldReferenceNode.Open); info.TrailingComments().Len() > 0 {
- f.writeInlineComments(info.TrailingComments())
- }
- f.writeInline(fieldReferenceNode.Name)
- } else {
- f.writeNode(fieldReferenceNode.Name)
- if info := f.nodeInfo(fieldReferenceNode.Name); info.TrailingComments().Len() > 0 {
- f.writeInlineComments(info.TrailingComments())
- }
- }
- if fieldReferenceNode.Close != nil {
- f.writeInline(fieldReferenceNode.Close)
- }
- continue
- }
- if i > 0 {
- // The length of this slice must be exactly len(Parts)-1.
- f.writeInline(optionNameNode.Dots[i-1])
- }
- f.writeNode(optionNameNode.Parts[i])
- }
-}
-
-// writeMessage writes the message node.
-//
-// For example,
-//
-// message Foo {
-// option deprecated = true;
-// reserved 50 to 100;
-// extensions 150 to 200;
-//
-// message Bar {
-// string name = 1;
-// }
-// enum Baz {
-// BAZ_UNSPECIFIED = 0;
-// }
-// extend Bar {
-// string value = 2;
-// }
-//
-// Bar bar = 1;
-// Baz baz = 2;
-// }
-func (f *formatter) writeMessage(messageNode *ast.MessageNode) {
- // Track FQN for deprecation
- popFQN := f.pushFQN(messageNode.Name.Val)
- defer popFQN()
-
- var elementWriterFunc func()
- if len(messageNode.Decls) != 0 {
- // Check if we need to inject deprecation
- needsDeprecation := f.shouldInjectDeprecation(f.currentFQN()) && !hasDeprecatedOption(messageNode.Decls)
- elementWriterFunc = func() {
- if needsDeprecation {
- f.writeDeprecatedOption()
- }
- for _, decl := range messageNode.Decls {
- f.writeNode(decl)
- }
- }
- } else if f.shouldInjectDeprecation(f.currentFQN()) {
- // Empty message that needs deprecation
- elementWriterFunc = func() {
- f.writeDeprecatedOption()
- }
- }
- if messageNode.Visibility != nil {
- f.writeStart(messageNode.Visibility, false)
- f.Space()
- f.writeInline(messageNode.Keyword)
- } else {
- f.writeStart(messageNode.Keyword, false)
- }
- f.Space()
- f.writeInline(messageNode.Name)
- f.Space()
- f.writeCompositeTypeBody(
- messageNode.OpenBrace,
- messageNode.CloseBrace,
- elementWriterFunc,
- )
-}
-
-// writeMessageLiteral writes a message literal.
-//
-// For example,
-//
-// {
-// foo: 1
-// foo: 2
-// foo: 3
-// bar: <
-// name:"abc"
-// id:123
-// >
-// }
-func (f *formatter) writeMessageLiteral(messageLiteralNode *ast.MessageLiteralNode) {
- if f.maybeWriteCompactMessageLiteral(messageLiteralNode, false) {
- return
- }
- var elementWriterFunc func()
- if len(messageLiteralNode.Elements) > 0 {
- elementWriterFunc = func() {
- f.writeMessageLiteralElements(messageLiteralNode)
- }
- }
- closeNode := messageLiteralClose(messageLiteralNode)
- // In the case where we are writing a compact message literal, we need to check if there
- // are any trailing comments on the message literal that needs to be handled. The trailing
- // comments may end up on the message literal node itself rather than the closing bracket
- // in the case where a message literal is an element of a message literal, and trailing
- // comments on the separator are attached to message literal.
- messageLiteralTrailingComments := f.nodeInfo(messageLiteralNode).TrailingComments()
- // Similar to how we handle the trailing comments on separators, we check if there are
- // any existing trailing comments on the closing bracket. If not, then we attach the trailing
- // comments of the message literal node to the closing bracket. Skip this if the closing
- // bracket already has trailing comments.
- if messageLiteralTrailingComments.Len() > 0 && f.nodeInfo(closeNode).TrailingComments().Len() == 0 {
- f.setTrailingComments(closeNode, messageLiteralTrailingComments)
- }
- f.writeCompositeValueBody(
- messageLiteralOpen(messageLiteralNode),
- closeNode,
- elementWriterFunc,
- )
-}
-
-// writeMessageLiteral writes a message literal suitable for
-// an element in an array literal.
-func (f *formatter) writeMessageLiteralForArray(
- messageLiteralNode *ast.MessageLiteralNode,
- lastElement bool,
-) {
- if f.maybeWriteCompactMessageLiteral(messageLiteralNode, true) {
- return
- }
- var elementWriterFunc func()
- if len(messageLiteralNode.Elements) > 0 {
- elementWriterFunc = func() {
- f.writeMessageLiteralElements(messageLiteralNode)
- }
- }
- closeWriter := f.writeBodyEndInline
- if lastElement {
- closeWriter = f.writeBodyEnd
- }
- closeNode := messageLiteralClose(messageLiteralNode)
- // In the case where we are writing a compact message literal, we need to check if there
- // are any trailing comments on the message literal that needs to be handled. The trailing
- // comments may end up on the message literal node itself rather than the closing bracket
- // in the case where a message literal is an element of a message literal, and trailing
- // comments on the separator are attached to message literal.
- messageLiteralTrailingComments := f.nodeInfo(messageLiteralNode).TrailingComments()
- // Similar to how we handle the trailing comments on separators, we check if there are
- // any existing trailing comments on the closing bracket. If not, then we attach the trailing
- // comments of the message literal node to the closing bracket. Skip this if the closing
- // bracket already has trailing comments.
- if messageLiteralTrailingComments.Len() > 0 && f.nodeInfo(closeNode).TrailingComments().Len() == 0 {
- f.setTrailingComments(closeNode, messageLiteralTrailingComments)
- }
- f.writeBody(
- messageLiteralOpen(messageLiteralNode),
- closeNode,
- elementWriterFunc,
- f.writeOpenBracePrefixForArray,
- closeWriter,
- )
-}
-
-func (f *formatter) maybeWriteCompactMessageLiteral(
- messageLiteralNode *ast.MessageLiteralNode,
- inArrayLiteral bool,
-) bool {
- // We only want to write a compact message literal for a message literal with either 0 or
- // 1 elements, and if there are no interior comments, and the element, if present, is not
- // a message or array literal.
- if len(messageLiteralNode.Elements) > 1 ||
- f.hasInteriorComments(messageLiteralNode.Children()...) ||
- messageLiteralHasNestedMessageOrArray(messageLiteralNode) {
- return false
- }
-
- // messages with a single scalar field and no comments can be
- // printed all on one line
- openNode := messageLiteralOpen(messageLiteralNode)
- closeNode := messageLiteralClose(messageLiteralNode)
-
- // In the case where we are writing a compact message literal, we need to check if there
- // are any trailing comments on the message literal that needs to be handled. The trailing
- // comments may end up on the message literal node itself rather than the closing bracket
- // in the case where a message literal is an element of a message literal, and trailing
- // comments on the separator are attached to message literal.
- messageLiteralTrailingComments := f.nodeInfo(messageLiteralNode).TrailingComments()
- // Similar to how we handle the trailing comments on separators, we check if there are
- // any existing trailing comments on the closing bracket. If not, then we attach the trailing
- // comments of the message literal node to the closing bracket. Skip this if the closing
- // bracket already has trailing comments.
- if messageLiteralTrailingComments.Len() > 0 && f.nodeInfo(closeNode).TrailingComments().Len() == 0 {
- f.setTrailingComments(closeNode, messageLiteralTrailingComments)
- }
-
- if inArrayLiteral {
- f.Indent(openNode)
- }
- f.writeInline(openNode)
- // We check if our compact message has one element, if so, write that value as compact.
- if len(messageLiteralNode.Elements) == 1 {
- fieldNode := messageLiteralNode.Elements[0]
- f.writeInline(fieldNode.Name)
- if fieldNode.Sep != nil {
- f.writeInline(fieldNode.Sep)
- } else {
- f.WriteString(":")
- }
- f.Space()
- if messageLiteralNode.Seps[0] != nil {
- // We are dropping the optional trailing separator. If it had
- // trailing comments and the value does not, move the separator's
- // trailing comment to the value.
- sepTrailingComments := f.nodeInfo(messageLiteralNode.Seps[0]).TrailingComments()
- if sepTrailingComments.Len() > 0 &&
- f.nodeInfo(fieldNode.Val).TrailingComments().Len() == 0 {
- f.setTrailingComments(fieldNode.Val, sepTrailingComments)
- }
- }
- f.writeInline(fieldNode.Val)
- }
- f.writeInline(closeNode)
- return true
-}
-
-func messageLiteralHasNestedMessageOrArray(messageLiteralNode *ast.MessageLiteralNode) bool {
- for _, elem := range messageLiteralNode.Elements {
- switch elem.Val.(type) {
- case *ast.ArrayLiteralNode, *ast.MessageLiteralNode:
- return true
- }
- }
- return false
-}
-
-func arrayLiteralHasNestedMessageOrArray(arrayLiteralNode *ast.ArrayLiteralNode) bool {
- for _, elem := range arrayLiteralNode.Elements {
- switch elem.(type) {
- case *ast.ArrayLiteralNode, *ast.MessageLiteralNode:
- return true
- }
- }
- return false
-}
-
-// writeMessageLiteralElements writes the message literal's elements.
-//
-// For example,
-//
-// foo: 1
-// foo: 2
-func (f *formatter) writeMessageLiteralElements(messageLiteralNode *ast.MessageLiteralNode) {
- for i := range messageLiteralNode.Elements {
- // Separators ("," or ";") are optional. To avoid inconsistent formatted output,
- // we suppress them, since they aren't needed. So we just write the element and
- // ignore any optional separator in the AST.
- if messageLiteralNode.Seps[i] != nil {
- // Since we are dropping the optional trailing separator, we should
- // possibly move its trailing comment to the element value so we don't
- // lose it. Skip this step if the value already has a trailing comment.
- sepTrailingComments := f.nodeInfo(messageLiteralNode.Seps[i]).TrailingComments()
- if sepTrailingComments.Len() > 0 &&
- f.nodeInfo(messageLiteralNode.Elements[i].Val).TrailingComments().Len() == 0 {
- f.setTrailingComments(messageLiteralNode.Elements[i].Val, sepTrailingComments)
- }
- }
- f.writeNode(messageLiteralNode.Elements[i])
- }
-}
-
-// writeMessageField writes the message field node, and concludes the
-// line without leaving room for a trailing separator in the parent
-// message literal.
-func (f *formatter) writeMessageField(messageFieldNode *ast.MessageFieldNode) {
- f.writeMessageFieldPrefix(messageFieldNode)
- if compoundStringLiteral, ok := messageFieldNode.Val.(*ast.CompoundStringLiteralNode); ok {
- f.writeCompoundStringLiteralIndent(compoundStringLiteral)
- return
- }
- f.writeLineEnd(messageFieldNode.Val)
-}
-
-// writeMessageFieldPrefix writes the message field node as a single line.
-//
-// For example,
-//
-// foo:"bar"
-func (f *formatter) writeMessageFieldPrefix(messageFieldNode *ast.MessageFieldNode) {
- // The comments need to be written as a multiline comment above
- // the message field name.
- //
- // Note that this is different than how field reference nodes are
- // normally formatted in-line (i.e. as option name components).
- fieldReferenceNode := messageFieldNode.Name
- if fieldReferenceNode.Open != nil {
- f.writeStart(fieldReferenceNode.Open, false)
- if fieldReferenceNode.URLPrefix != nil {
- f.writeInline(fieldReferenceNode.URLPrefix)
- f.writeInline(fieldReferenceNode.Slash)
- }
- f.writeInline(fieldReferenceNode.Name)
- } else {
- f.writeStart(fieldReferenceNode.Name, false)
- }
- if fieldReferenceNode.Close != nil {
- f.writeInline(fieldReferenceNode.Close)
- }
- // The colon separator is optional sometimes, but we don't have enough
- // information here to know whether it's necessary. For more consistent
- // output, just always include it.
- if messageFieldNode.Sep != nil {
- f.writeInline(messageFieldNode.Sep)
- } else {
- f.WriteString(":")
- }
- f.Space()
-}
-
-// writeEnum writes the enum node.
-//
-// For example,
-//
-// enum Foo {
-// option deprecated = true;
-// reserved 1 to 5;
-//
-// FOO_UNSPECIFIED = 0;
-// }
-func (f *formatter) writeEnum(enumNode *ast.EnumNode) {
- // Track FQN for deprecation
- popFQN := f.pushFQN(enumNode.Name.Val)
- defer popFQN()
-
- var elementWriterFunc func()
- if len(enumNode.Decls) > 0 {
- // Check if we need to inject deprecation
- needsDeprecation := f.shouldInjectDeprecation(f.currentFQN()) && !hasDeprecatedOption(enumNode.Decls)
- elementWriterFunc = func() {
- if needsDeprecation {
- f.writeDeprecatedOption()
- }
- for _, decl := range enumNode.Decls {
- f.writeNode(decl)
- }
- }
- } else if f.shouldInjectDeprecation(f.currentFQN()) {
- // Empty enum that needs deprecation
- elementWriterFunc = func() {
- f.writeDeprecatedOption()
- }
- }
- if enumNode.Visibility != nil {
- f.writeStart(enumNode.Visibility, false)
- f.Space()
- f.writeInline(enumNode.Keyword)
- } else {
- f.writeStart(enumNode.Keyword, false)
- }
- f.Space()
- f.writeInline(enumNode.Name)
- f.Space()
- f.writeCompositeTypeBody(
- enumNode.OpenBrace,
- enumNode.CloseBrace,
- elementWriterFunc,
- )
-}
-
-// writeEnumValue writes the enum value as a single line. If the enum has
-// compact options, it will be written across multiple lines.
-//
-// For example,
-//
-// FOO_UNSPECIFIED = 1 [
-// deprecated = true
-// ];
-func (f *formatter) writeEnumValue(enumValueNode *ast.EnumValueNode) {
- f.writeStart(enumValueNode.Name, false)
- f.Space()
- f.writeInline(enumValueNode.Equals)
- f.Space()
- f.writeInline(enumValueNode.Number)
- // Check if we need to inject deprecation for this enum value (exact match only)
- // Enum values are scoped to their parent (message or package), NOT the enum itself.
- // So we use the parent FQN (without the enum name) + the enum value name.
- enumValueFQN := parentFQN(f.packageFQN) + "." + enumValueNode.Name.Val
- needsDeprecation := f.shouldInjectDeprecationExact(enumValueFQN) &&
- !hasCompactDeprecatedOption(enumValueNode.Options)
- if enumValueNode.Options != nil {
- f.Space()
- if needsDeprecation {
- f.injectCompactDeprecation = true
- }
- f.writeNode(enumValueNode.Options)
- f.injectCompactDeprecation = false
- } else if needsDeprecation {
- f.writeCompactDeprecatedOption()
- }
- f.writeLineEnd(enumValueNode.Semicolon)
-}
-
-// writeField writes the field node as a single line. If the field has
-// compact options, it will be written across multiple lines.
-//
-// For example,
-//
-// repeated string name = 1 [
-// deprecated = true,
-// json_name = "name"
-// ];
-func (f *formatter) writeField(fieldNode *ast.FieldNode) {
- // We need to handle the comments for the field label specially since
- // a label might not be defined, but it has the leading comments attached
- // to it.
- if fieldNode.Label.KeywordNode != nil {
- f.writeStart(fieldNode.Label, false)
- f.Space()
- f.writeInline(fieldNode.FldType)
- } else {
- // If a label was not written, the multiline comments will be
- // attached to the type.
- if compoundIdentNode, ok := fieldNode.FldType.(*ast.CompoundIdentNode); ok {
- f.writeCompoundIdentForFieldName(compoundIdentNode)
- } else {
- f.writeStart(fieldNode.FldType, false)
- }
- }
- f.Space()
- f.writeInline(fieldNode.Name)
- f.Space()
- if fieldNode.Equals != nil {
- f.writeInline(fieldNode.Equals)
- f.Space()
- }
- if fieldNode.Tag != nil {
- f.writeInline(fieldNode.Tag)
- }
- // Check if we need to inject deprecation for this field (exact match only)
- fieldFQN := f.currentFQN() + "." + fieldNode.Name.Val
- needsDeprecation := f.shouldInjectDeprecationExact(fieldFQN) &&
- !hasCompactDeprecatedOption(fieldNode.Options)
- if fieldNode.Options != nil {
- f.Space()
- if needsDeprecation {
- f.injectCompactDeprecation = true
- }
- f.writeNode(fieldNode.Options)
- f.injectCompactDeprecation = false
- } else if needsDeprecation {
- f.writeCompactDeprecatedOption()
- }
- f.writeLineEnd(fieldNode.Semicolon)
-}
-
-// writeMapField writes a map field (e.g. 'map pairs = 1;').
-func (f *formatter) writeMapField(mapFieldNode *ast.MapFieldNode) {
- f.writeNode(mapFieldNode.MapType)
- f.Space()
- f.writeInline(mapFieldNode.Name)
- f.Space()
- f.writeInline(mapFieldNode.Equals)
- f.Space()
- f.writeInline(mapFieldNode.Tag)
- if mapFieldNode.Options != nil {
- f.Space()
- f.writeNode(mapFieldNode.Options)
- }
- f.writeLineEnd(mapFieldNode.Semicolon)
-}
-
-// writeMapType writes a map type (e.g. 'map').
-func (f *formatter) writeMapType(mapTypeNode *ast.MapTypeNode) {
- f.writeStart(mapTypeNode.Keyword, false)
- f.writeInline(mapTypeNode.OpenAngle)
- f.writeInline(mapTypeNode.KeyType)
- f.writeInline(mapTypeNode.Comma)
- f.Space()
- f.writeInline(mapTypeNode.ValueType)
- f.writeInline(mapTypeNode.CloseAngle)
-}
-
-// writeFieldReference writes a field reference (e.g. '(foo.bar)').
-func (f *formatter) writeFieldReference(fieldReferenceNode *ast.FieldReferenceNode) {
- if fieldReferenceNode.Open != nil {
- f.writeInline(fieldReferenceNode.Open)
- }
- f.writeInline(fieldReferenceNode.Name)
- if fieldReferenceNode.Close != nil {
- f.writeInline(fieldReferenceNode.Close)
- }
-}
-
-// writeExtend writes the extend node.
-//
-// For example,
-//
-// extend google.protobuf.FieldOptions {
-// bool redacted = 33333;
-// }
-func (f *formatter) writeExtend(extendNode *ast.ExtendNode) {
- var elementWriterFunc func()
- if len(extendNode.Decls) > 0 {
- elementWriterFunc = func() {
- for _, decl := range extendNode.Decls {
- f.writeNode(decl)
- }
- }
- }
- f.writeStart(extendNode.Keyword, false)
- f.Space()
- f.writeInline(extendNode.Extendee)
- f.Space()
- f.writeCompositeTypeBody(
- extendNode.OpenBrace,
- extendNode.CloseBrace,
- elementWriterFunc,
- )
-}
-
-// writeService writes the service node.
-//
-// For example,
-//
-// service FooService {
-// option deprecated = true;
-//
-// rpc Foo(FooRequest) returns (FooResponse) {};
-func (f *formatter) writeService(serviceNode *ast.ServiceNode) {
- // Track FQN for deprecation
- popFQN := f.pushFQN(serviceNode.Name.Val)
- defer popFQN()
-
- var elementWriterFunc func()
- if len(serviceNode.Decls) > 0 {
- // Check if we need to inject deprecation
- needsDeprecation := f.shouldInjectDeprecation(f.currentFQN()) && !hasDeprecatedOption(serviceNode.Decls)
- elementWriterFunc = func() {
- if needsDeprecation {
- f.writeDeprecatedOption()
- }
- for _, decl := range serviceNode.Decls {
- f.writeNode(decl)
- }
- }
- } else if f.shouldInjectDeprecation(f.currentFQN()) {
- // Empty service that needs deprecation
- elementWriterFunc = func() {
- f.writeDeprecatedOption()
- }
- }
- f.writeStart(serviceNode.Keyword, false)
- f.Space()
- f.writeInline(serviceNode.Name)
- f.Space()
- f.writeCompositeTypeBody(
- serviceNode.OpenBrace,
- serviceNode.CloseBrace,
- elementWriterFunc,
- )
-}
-
-// writeRPC writes the RPC node. RPCs are formatted in
-// the following order:
-//
-// For example,
-//
-// rpc Foo(FooRequest) returns (FooResponse) {
-// option deprecated = true;
-// };
-func (f *formatter) writeRPC(rpcNode *ast.RPCNode) {
- // Track FQN for deprecation (RPCs are children of services)
- popFQN := f.pushFQN(rpcNode.Name.Val)
- defer popFQN()
-
- needsDeprecation := f.shouldInjectDeprecation(f.currentFQN()) && !hasDeprecatedOption(rpcNode.Decls)
-
- var elementWriterFunc func()
- if len(rpcNode.Decls) > 0 {
- elementWriterFunc = func() {
- if needsDeprecation {
- f.writeDeprecatedOption()
- }
- for _, decl := range rpcNode.Decls {
- f.writeNode(decl)
- }
- }
- } else if needsDeprecation {
- elementWriterFunc = func() {
- f.writeDeprecatedOption()
- }
- }
- f.writeStart(rpcNode.Keyword, false)
- f.Space()
- f.writeInline(rpcNode.Name)
- f.writeInline(rpcNode.Input)
- f.Space()
- f.writeInline(rpcNode.Returns)
- f.Space()
- f.writeInline(rpcNode.Output)
- if rpcNode.OpenBrace == nil && !needsDeprecation {
- // This RPC doesn't have any elements and doesn't need deprecation,
- // so we prefer the ';' form.
- //
- // rpc Ping(PingRequest) returns (PingResponse);
- //
- f.writeLineEnd(rpcNode.Semicolon)
- return
- }
- // If we need to add deprecation to an RPC without a body, we need to
- // create a body for it.
- f.Space()
- if rpcNode.OpenBrace != nil {
- f.writeCompositeTypeBody(
- rpcNode.OpenBrace,
- rpcNode.CloseBrace,
- elementWriterFunc,
- )
- } else {
- // RPC had no body but needs deprecation - create synthetic body
- f.WriteString("{")
- f.P("")
- f.In()
- if elementWriterFunc != nil {
- elementWriterFunc()
- }
- f.Out()
- f.Indent(nil)
- f.WriteString("}")
- f.P("")
- }
-}
-
-// writeRPCType writes the RPC type node (e.g. (stream foo.Bar)).
-func (f *formatter) writeRPCType(rpcTypeNode *ast.RPCTypeNode) {
- f.writeInline(rpcTypeNode.OpenParen)
- if rpcTypeNode.Stream != nil {
- f.writeInline(rpcTypeNode.Stream)
- f.Space()
- }
- f.writeInline(rpcTypeNode.MessageType)
- f.writeInline(rpcTypeNode.CloseParen)
-}
-
-// writeOneOf writes the oneof node.
-//
-// For example,
-//
-// oneof foo {
-// option deprecated = true;
-//
-// string name = 1;
-// int number = 2;
-// }
-func (f *formatter) writeOneOf(oneOfNode *ast.OneofNode) {
- var elementWriterFunc func()
- if len(oneOfNode.Decls) > 0 {
- elementWriterFunc = func() {
- for _, decl := range oneOfNode.Decls {
- f.writeNode(decl)
- }
- }
- }
- f.writeStart(oneOfNode.Keyword, false)
- f.Space()
- f.writeInline(oneOfNode.Name)
- f.Space()
- f.writeCompositeTypeBody(
- oneOfNode.OpenBrace,
- oneOfNode.CloseBrace,
- elementWriterFunc,
- )
-}
-
-// writeGroup writes the group node.
-//
-// For example,
-//
-// optional group Key = 4 [
-// deprecated = true,
-// json_name = "key"
-// ] {
-// optional uint64 id = 1;
-// optional string name = 2;
-// }
-func (f *formatter) writeGroup(groupNode *ast.GroupNode) {
- var elementWriterFunc func()
- if len(groupNode.Decls) > 0 {
- elementWriterFunc = func() {
- for _, decl := range groupNode.Decls {
- f.writeNode(decl)
- }
- }
- }
- // We need to handle the comments for the group label specially since
- // a label might not be defined, but it has the leading comments attached
- // to it.
- if groupNode.Label.KeywordNode != nil {
- f.writeStart(groupNode.Label, false)
- f.Space()
- f.writeInline(groupNode.Keyword)
- } else {
- // If a label was not written, the multiline comments will be
- // attached to the keyword.
- f.writeStart(groupNode.Keyword, false)
- }
- f.Space()
- f.writeInline(groupNode.Name)
- f.Space()
- f.writeInline(groupNode.Equals)
- f.Space()
- f.writeInline(groupNode.Tag)
- if groupNode.Options != nil {
- f.Space()
- f.writeNode(groupNode.Options)
- }
- f.Space()
- f.writeCompositeTypeBody(
- groupNode.OpenBrace,
- groupNode.CloseBrace,
- elementWriterFunc,
- )
-}
-
-// writeExtensionRange writes the extension range node.
-//
-// For example,
-//
-// extensions 5-10, 100 to max [
-// deprecated = true
-// ];
-func (f *formatter) writeExtensionRange(extensionRangeNode *ast.ExtensionRangeNode) {
- f.writeStart(extensionRangeNode.Keyword, false)
- f.Space()
- for i := range extensionRangeNode.Ranges {
- if i > 0 {
- // The length of this slice must be exactly len(Ranges)-1.
- f.writeInline(extensionRangeNode.Commas[i-1])
- f.Space()
- }
- f.writeNode(extensionRangeNode.Ranges[i])
- }
- if extensionRangeNode.Options != nil {
- f.Space()
- f.writeNode(extensionRangeNode.Options)
- }
- f.writeLineEnd(extensionRangeNode.Semicolon)
-}
-
-// writeReserved writes a reserved node.
-//
-// For example,
-//
-// reserved 5-10, 100 to max;
-func (f *formatter) writeReserved(reservedNode *ast.ReservedNode) {
- f.writeStart(reservedNode.Keyword, false)
- // Either names or ranges will be set, but never both.
- elements := make([]ast.Node, 0, len(reservedNode.Names)+len(reservedNode.Ranges))
- switch {
- case reservedNode.Names != nil:
- for _, nameNode := range reservedNode.Names {
- elements = append(elements, nameNode)
- }
- case reservedNode.Identifiers != nil:
- for _, identNode := range reservedNode.Identifiers {
- elements = append(elements, identNode)
- }
- case reservedNode.Ranges != nil:
- for _, rangeNode := range reservedNode.Ranges {
- elements = append(elements, rangeNode)
- }
- }
- f.Space()
- for i := range elements {
- if i > 0 {
- // The length of this slice must be exactly len({Names,Ranges})-1.
- f.writeInline(reservedNode.Commas[i-1])
- f.Space()
- }
- f.writeInline(elements[i])
- }
- f.writeLineEnd(reservedNode.Semicolon)
-}
-
-// writeRange writes the given range node (e.g. '1 to max').
-func (f *formatter) writeRange(rangeNode *ast.RangeNode) {
- f.writeInline(rangeNode.StartVal)
- if rangeNode.To != nil {
- f.Space()
- f.writeInline(rangeNode.To)
- }
- // Either EndVal or Max will be set, but never both.
- switch {
- case rangeNode.EndVal != nil:
- f.Space()
- f.writeInline(rangeNode.EndVal)
- case rangeNode.Max != nil:
- f.Space()
- f.writeInline(rangeNode.Max)
- }
-}
-
-// writeCompactOptions writes a compact options node.
-//
-// For example,
-//
-// [
-// deprecated = true,
-// json_name = "something"
-// ]
-func (f *formatter) writeCompactOptions(compactOptionsNode *ast.CompactOptionsNode) {
- f.inCompactOptions = true
- defer func() {
- f.inCompactOptions = false
- }()
- // If we need to inject deprecation, we must use multiline format
- injectDeprecation := f.injectCompactDeprecation
- if len(compactOptionsNode.Options) == 1 && !injectDeprecation &&
- !f.hasInteriorComments(compactOptionsNode.OpenBracket, compactOptionsNode.Options[0].Name) {
- // If there's only a single compact scalar option without comments, we can write it
- // in-line. For example:
- //
- // [deprecated = true]
- //
- // However, this does not include the case when the '[' has trailing comments,
- // or the option name has leading comments. In those cases, we write the option
- // across multiple lines. For example:
- //
- // [
- // // This type is deprecated.
- // deprecated = true
- // ]
- //
- optionNode := compactOptionsNode.Options[0]
- f.writeInline(compactOptionsNode.OpenBracket)
- f.writeInline(optionNode.Name)
- f.Space()
- f.writeInline(optionNode.Equals)
- if node, ok := optionNode.Val.(*ast.CompoundStringLiteralNode); ok {
- // If there's only a single compact option, the value needs to
- // write its comments (if any) in a way that preserves the closing ']'.
- f.writeCompoundStringLiteralNoIndentEndInline(node)
- f.writeInline(compactOptionsNode.CloseBracket)
- return
- }
- f.Space()
- f.writeInline(optionNode.Val)
- f.writeInline(compactOptionsNode.CloseBracket)
- return
- }
- var elementWriterFunc func()
- if len(compactOptionsNode.Options) > 0 || injectDeprecation {
- elementWriterFunc = func() {
- // If we need to inject deprecation, write it first
- if injectDeprecation {
- if len(compactOptionsNode.Options) > 0 {
- f.P("deprecated = true,")
- } else {
- f.P("deprecated = true")
- }
- }
- for i, opt := range compactOptionsNode.Options {
- if i == len(compactOptionsNode.Options)-1 {
- // The last element won't have a trailing comma.
- f.writeLastCompactOption(opt)
- return
- }
- f.writeNode(opt)
- f.writeLineEnd(compactOptionsNode.Commas[i])
- }
- }
- }
- f.writeCompositeValueBody(
- compactOptionsNode.OpenBracket,
- compactOptionsNode.CloseBracket,
- elementWriterFunc,
- )
-}
-
-func (f *formatter) hasInteriorComments(nodes ...ast.Node) bool {
- for i, n := range nodes {
- // interior comments mean we ignore leading comments on first
- // token and trailing comments on the last one
- info := f.nodeInfo(n)
- if i > 0 && info.LeadingComments().Len() > 0 {
- return true
- }
- if i < len(nodes)-1 && info.TrailingComments().Len() > 0 {
- return true
- }
- }
- return false
-}
-
-// writeArrayLiteral writes an array literal across multiple lines.
-//
-// For example,
-//
-// [
-// "foo",
-// "bar"
-// ]
-func (f *formatter) writeArrayLiteral(arrayLiteralNode *ast.ArrayLiteralNode) {
- // We need to check if there are any trailing comments on the array literal itself that
- // needs to be handled. The trailing comments may end up on the array literal node itself
- // rather than the closing bracket in the case where an array literal is an element of a
- // message literal, and trailing comments on the separator are attached to array literal.
- arrayLiteralTrailingComments := f.nodeInfo(arrayLiteralNode).TrailingComments()
- // Similar to how we handle trailing comments on separators, we check if there are any
- // existing trailing comments on the closing bracket. If not, then we attach the trailing
- // comments of the array literal node to the closing bracket. Skip this if the closing
- // bracket already has trailing comments.
- if arrayLiteralTrailingComments.Len() > 0 && f.nodeInfo(arrayLiteralNode.CloseBracket).TrailingComments().Len() == 0 {
- f.setTrailingComments(arrayLiteralNode.CloseBracket, arrayLiteralTrailingComments)
- }
-
- if len(arrayLiteralNode.Elements) == 1 &&
- !f.hasInteriorComments(arrayLiteralNode.Children()...) &&
- !arrayLiteralHasNestedMessageOrArray(arrayLiteralNode) {
- // arrays with a single scalar value and no comments can be
- // printed all on one line
- valueNode := arrayLiteralNode.Elements[0]
- f.writeInline(arrayLiteralNode.OpenBracket)
- f.writeInline(valueNode)
- f.writeInline(arrayLiteralNode.CloseBracket)
- return
- }
-
- var elementWriterFunc func()
- if len(arrayLiteralNode.Elements) > 0 {
- elementWriterFunc = func() {
- for i := range arrayLiteralNode.Elements {
- lastElement := i == len(arrayLiteralNode.Elements)-1
- if compositeNode, ok := arrayLiteralNode.Elements[i].(ast.CompositeNode); ok {
- f.writeCompositeValueForArrayLiteral(compositeNode, lastElement)
- if !lastElement {
- f.writeLineEnd(arrayLiteralNode.Commas[i])
- }
- continue
- }
- if lastElement {
- // The last element won't have a trailing comma.
- f.writeLineElement(arrayLiteralNode.Elements[i])
- return
- }
- f.writeStart(arrayLiteralNode.Elements[i], false)
- f.writeLineEnd(arrayLiteralNode.Commas[i])
- }
- }
- }
-
- f.writeCompositeValueBody(
- arrayLiteralNode.OpenBracket,
- arrayLiteralNode.CloseBracket,
- elementWriterFunc,
- )
-}
-
-// writeCompositeForArrayLiteral writes the composite node in a way that's suitable
-// for array literals. In general, signed integers and compound strings should have their
-// comments written in-line because they are one of many components in a single line.
-//
-// However, each of these composite types occupy a single line in an array literal,
-// so they need their comments to be formatted like a standalone node.
-//
-// For example,
-//
-// option (value) = /* In-line comment for '-42' */ -42;
-//
-// option (thing) = {
-// values: [
-// // Leading comment on -42.
-// -42, // Trailing comment on -42.
-// ]
-// }
-//
-// The lastElement boolean is used to signal whether or not the composite value
-// should be written as the last element (i.e. it doesn't have a trailing comma).
-func (f *formatter) writeCompositeValueForArrayLiteral(
- compositeNode ast.CompositeNode,
- lastElement bool,
-) {
- switch node := compositeNode.(type) {
- case *ast.CompoundStringLiteralNode:
- f.writeCompoundStringLiteralForArray(node, lastElement)
- case *ast.NegativeIntLiteralNode:
- f.writeNegativeIntLiteralForArray(node, lastElement)
- case *ast.SignedFloatLiteralNode:
- f.writeSignedFloatLiteralForArray(node, lastElement)
- case *ast.MessageLiteralNode:
- f.writeMessageLiteralForArray(node, lastElement)
- default:
- f.err = errors.Join(f.err, fmt.Errorf("unexpected array value node %T", node))
- }
-}
-
-// writeCompositeTypeBody writes the body of a composite type, e.g. message, enum, extend, oneof, etc.
-func (f *formatter) writeCompositeTypeBody(
- openBrace *ast.RuneNode,
- closeBrace *ast.RuneNode,
- elementWriterFunc func(),
-) {
- f.writeBody(
- openBrace,
- closeBrace,
- elementWriterFunc,
- f.writeOpenBracePrefix,
- f.writeBodyEnd,
- )
-}
-
-// writeCompositeValueBody writes the body of a composite value, e.g. compact options,
-// array literal, etc. We need to handle the ']' different than composite types because
-// there could be more tokens following the final ']'.
-func (f *formatter) writeCompositeValueBody(
- openBrace *ast.RuneNode,
- closeBrace *ast.RuneNode,
- elementWriterFunc func(),
-) {
- f.writeBody(
- openBrace,
- closeBrace,
- elementWriterFunc,
- f.writeOpenBracePrefix,
- f.writeBodyEndInline,
- )
-}
-
-// writeBody writes the body of a type or value, e.g. message, enum, compact options, etc.
-// The elementWriterFunc is used to write the declarations within the composite type (e.g.
-// fields in a message). The openBraceWriterFunc and closeBraceWriterFunc functions are used
-// to customize how the '{' and '} nodes are written, respectively.
-func (f *formatter) writeBody(
- openBrace *ast.RuneNode,
- closeBrace *ast.RuneNode,
- elementWriterFunc func(),
- openBraceWriterFunc func(ast.Node),
- closeBraceWriterFunc func(ast.Node, bool),
-) {
- if elementWriterFunc == nil && !f.hasInteriorComments(openBrace, closeBrace) {
- // completely empty body
- f.writeInline(openBrace)
- closeBraceWriterFunc(closeBrace, true)
- return
- }
-
- openBraceWriterFunc(openBrace)
- if elementWriterFunc != nil {
- elementWriterFunc()
- }
- closeBraceWriterFunc(closeBrace, false)
-}
-
-// writeOpenBracePrefix writes the open brace with its leading comments in-line.
-// This is used for nearly every use case of f.writeBody, excluding the instances
-// in array literals.
-func (f *formatter) writeOpenBracePrefix(openBrace ast.Node) {
- defer f.SetPreviousNode(openBrace)
- info := f.nodeInfo(openBrace)
- if info.LeadingComments().Len() > 0 {
- f.writeInlineComments(info.LeadingComments())
- if info.LeadingWhitespace() != "" {
- f.Space()
- }
- }
- f.writeNode(openBrace)
- if info.TrailingComments().Len() > 0 {
- f.writeTrailingEndComments(info.TrailingComments())
- } else {
- f.P("")
- }
-}
-
-// writeOpenBracePrefixForArray writes the open brace with its leading comments
-// on multiple lines. This is only used for message literals in arrays.
-func (f *formatter) writeOpenBracePrefixForArray(openBrace ast.Node) {
- defer f.SetPreviousNode(openBrace)
- info := f.nodeInfo(openBrace)
- if info.LeadingComments().Len() > 0 {
- f.writeMultilineComments(info.LeadingComments())
- }
- f.Indent(openBrace)
- f.writeNode(openBrace)
- if info.TrailingComments().Len() > 0 {
- f.writeTrailingEndComments(info.TrailingComments())
- } else {
- f.P("")
- }
-}
-
-// writeCompoundIdent writes a compound identifier (e.g. '.com.foo.Bar').
-func (f *formatter) writeCompoundIdent(compoundIdentNode *ast.CompoundIdentNode) {
- if compoundIdentNode.LeadingDot != nil {
- f.writeInline(compoundIdentNode.LeadingDot)
- }
- for i := range compoundIdentNode.Components {
- if i > 0 {
- // The length of this slice must be exactly len(Components)-1.
- f.writeInline(compoundIdentNode.Dots[i-1])
- }
- f.writeInline(compoundIdentNode.Components[i])
- }
-}
-
-// writeCompoundIdentForFieldName writes a compound identifier, but handles comments
-// specially for field names.
-//
-// For example,
-//
-// message Foo {
-// // These are comments attached to bar.
-// bar.v1.Bar bar = 1;
-// }
-func (f *formatter) writeCompoundIdentForFieldName(compoundIdentNode *ast.CompoundIdentNode) {
- if compoundIdentNode.LeadingDot != nil {
- f.writeStart(compoundIdentNode.LeadingDot, false)
- }
- for i := range compoundIdentNode.Components {
- if i == 0 && compoundIdentNode.LeadingDot == nil {
- f.writeStart(compoundIdentNode.Components[i], false)
- continue
- }
- if i > 0 {
- // The length of this slice must be exactly len(Components)-1.
- f.writeInline(compoundIdentNode.Dots[i-1])
- }
- f.writeInline(compoundIdentNode.Components[i])
- }
-}
-
-// writeFieldLabel writes the field label node.
-//
-// For example,
-//
-// optional
-// repeated
-// required
-func (f *formatter) writeFieldLabel(fieldLabel ast.FieldLabel) {
- f.WriteString(fieldLabel.Val)
-}
-
-// writeCompoundStringLiteral writes a compound string literal value.
-//
-// For example,
-//
-// "one,"
-// "two,"
-// "three"
-func (f *formatter) writeCompoundStringLiteral(
- compoundStringLiteralNode *ast.CompoundStringLiteralNode,
- needsIndent bool,
- hasTrailingPunctuation bool,
-) {
- f.P("")
- if needsIndent {
- f.In()
- }
- for i, child := range compoundStringLiteralNode.Children() {
- if hasTrailingPunctuation && i == len(compoundStringLiteralNode.Children())-1 {
- // inline because there may be a subsequent comma or punctuation from enclosing element
- f.writeStart(child, false)
- break
- }
- f.writeLineElement(child)
- }
- if needsIndent {
- f.Out()
- }
-}
-
-func (f *formatter) writeCompoundStringLiteralIndent(
- compoundStringLiteralNode *ast.CompoundStringLiteralNode,
-) {
- f.writeCompoundStringLiteral(compoundStringLiteralNode, true, false)
-}
-
-func (f *formatter) writeCompoundStringLiteralIndentEndInline(
- compoundStringLiteralNode *ast.CompoundStringLiteralNode,
-) {
- f.writeCompoundStringLiteral(compoundStringLiteralNode, true, true)
-}
-
-func (f *formatter) writeCompoundStringLiteralNoIndentEndInline(
- compoundStringLiteralNode *ast.CompoundStringLiteralNode,
-) {
- f.writeCompoundStringLiteral(compoundStringLiteralNode, false, true)
-}
-
-// writeCompoundStringLiteralForArray writes a compound string literal value,
-// but writes its comments suitable for an element in an array literal.
-//
-// The lastElement boolean is used to signal whether or not the value should
-// be written as the last element (i.e. it doesn't have a trailing comma).
-func (f *formatter) writeCompoundStringLiteralForArray(
- compoundStringLiteralNode *ast.CompoundStringLiteralNode,
- lastElement bool,
-) {
- for i, child := range compoundStringLiteralNode.Children() {
- if !lastElement && i == len(compoundStringLiteralNode.Children())-1 {
- f.writeStart(child, false)
- return
- }
- f.writeLineElement(child)
- }
-}
-
-// writeFloatLiteral writes a float literal value (e.g. '42.2').
-func (f *formatter) writeFloatLiteral(floatLiteralNode *ast.FloatLiteralNode) {
- f.writeRaw(floatLiteralNode)
-}
-
-// writeSignedFloatLiteral writes a signed float literal value (e.g. '-42.2').
-func (f *formatter) writeSignedFloatLiteral(signedFloatLiteralNode *ast.SignedFloatLiteralNode) {
- f.writeInline(signedFloatLiteralNode.Sign)
- f.writeInline(signedFloatLiteralNode.Float)
-}
-
-// writeSignedFloatLiteralForArray writes a signed float literal value, but writes
-// its comments suitable for an element in an array literal.
-//
-// The lastElement boolean is used to signal whether or not the value should
-// be written as the last element (i.e. it doesn't have a trailing comma).
-func (f *formatter) writeSignedFloatLiteralForArray(
- signedFloatLiteralNode *ast.SignedFloatLiteralNode,
- lastElement bool,
-) {
- f.writeStart(signedFloatLiteralNode.Sign, false)
- if lastElement {
- f.writeLineEnd(signedFloatLiteralNode.Float)
- return
- }
- f.writeInline(signedFloatLiteralNode.Float)
-}
-
-// writeSpecialFloatLiteral writes a special float literal value (e.g. "nan" or "inf").
-func (f *formatter) writeSpecialFloatLiteral(specialFloatLiteralNode *ast.SpecialFloatLiteralNode) {
- f.WriteString(specialFloatLiteralNode.KeywordNode.Val)
-}
-
-// writeStringLiteral writes a string literal value (e.g. "foo").
-// Note that the raw string is written as-is so that it preserves
-// the quote style used in the original source.
-func (f *formatter) writeStringLiteral(stringLiteralNode *ast.StringLiteralNode) {
- f.writeRaw(stringLiteralNode)
-}
-
-// writeUintLiteral writes a uint literal (e.g. '42').
-func (f *formatter) writeUintLiteral(uintLiteralNode *ast.UintLiteralNode) {
- f.writeRaw(uintLiteralNode)
-}
-
-// writeNegativeIntLiteral writes a negative int literal (e.g. '-42').
-func (f *formatter) writeNegativeIntLiteral(negativeIntLiteralNode *ast.NegativeIntLiteralNode) {
- f.writeInline(negativeIntLiteralNode.Minus)
- f.writeInline(negativeIntLiteralNode.Uint)
-}
-
-func (f *formatter) writeRaw(n ast.Node) {
- info := f.nodeInfo(n)
- f.WriteString(info.RawText())
-}
-
-// writeNegativeIntLiteralForArray writes a negative int literal value, but writes
-// its comments suitable for an element in an array literal.
-//
-// The lastElement boolean is used to signal whether or not the value should
-// be written as the last element (i.e. it doesn't have a trailing comma).
-func (f *formatter) writeNegativeIntLiteralForArray(
- negativeIntLiteralNode *ast.NegativeIntLiteralNode,
- lastElement bool,
-) {
- f.writeStart(negativeIntLiteralNode.Minus, false)
- if lastElement {
- f.writeLineEnd(negativeIntLiteralNode.Uint)
- return
- }
- f.writeInline(negativeIntLiteralNode.Uint)
-}
-
-// writeIdent writes an identifier (e.g. 'foo').
-func (f *formatter) writeIdent(identNode *ast.IdentNode) {
- f.WriteString(identNode.Val)
-}
-
-// writeKeyword writes a keyword (e.g. 'syntax').
-func (f *formatter) writeKeyword(keywordNode *ast.KeywordNode) {
- f.WriteString(keywordNode.Val)
-}
-
-// writeRune writes a rune (e.g. '=').
-func (f *formatter) writeRune(runeNode *ast.RuneNode) {
- if strings.ContainsRune("{[(<", runeNode.Rune) {
- f.pendingIndent++
- } else if strings.ContainsRune("}])>", runeNode.Rune) {
- f.pendingIndent--
- }
- f.WriteString(string(runeNode.Rune))
-}
-
-// writeNode writes the node by dispatching to a function tailored to its concrete type.
-//
-// Comments are handled in each respective write function so that it can determine whether
-// to write the comments in-line or not.
-func (f *formatter) writeNode(node ast.Node) {
- switch element := node.(type) {
- case *ast.ArrayLiteralNode:
- f.writeArrayLiteral(element)
- case *ast.CompactOptionsNode:
- f.writeCompactOptions(element)
- case *ast.CompoundIdentNode:
- f.writeCompoundIdent(element)
- case *ast.CompoundStringLiteralNode:
- f.writeCompoundStringLiteralIndent(element)
- case *ast.EnumNode:
- f.writeEnum(element)
- case *ast.EnumValueNode:
- f.writeEnumValue(element)
- case *ast.ExtendNode:
- f.writeExtend(element)
- case *ast.ExtensionRangeNode:
- f.writeExtensionRange(element)
- case ast.FieldLabel:
- f.writeFieldLabel(element)
- case *ast.FieldNode:
- f.writeField(element)
- case *ast.FieldReferenceNode:
- f.writeFieldReference(element)
- case *ast.FloatLiteralNode:
- f.writeFloatLiteral(element)
- case *ast.GroupNode:
- f.writeGroup(element)
- case *ast.IdentNode:
- f.writeIdent(element)
- case *ast.ImportNode:
- // Make no assumptions on node ordering, set first == false to always preserve leading
- // whitespace.
- f.writeImport(element, false, false)
- case *ast.KeywordNode:
- f.writeKeyword(element)
- case *ast.MapFieldNode:
- f.writeMapField(element)
- case *ast.MapTypeNode:
- f.writeMapType(element)
- case *ast.MessageNode:
- f.writeMessage(element)
- case *ast.MessageFieldNode:
- f.writeMessageField(element)
- case *ast.MessageLiteralNode:
- f.writeMessageLiteral(element)
- case *ast.NegativeIntLiteralNode:
- f.writeNegativeIntLiteral(element)
- case *ast.OneofNode:
- f.writeOneOf(element)
- case *ast.OptionNode:
- f.writeOption(element)
- case *ast.OptionNameNode:
- f.writeOptionName(element)
- case *ast.PackageNode:
- // Make no assumptions on node ordering, set first == false to always preserve leading
- // whitespace.
- f.writePackage(element, false)
- case *ast.RangeNode:
- f.writeRange(element)
- case *ast.ReservedNode:
- f.writeReserved(element)
- case *ast.RPCNode:
- f.writeRPC(element)
- case *ast.RPCTypeNode:
- f.writeRPCType(element)
- case *ast.RuneNode:
- f.writeRune(element)
- case *ast.ServiceNode:
- f.writeService(element)
- case *ast.SignedFloatLiteralNode:
- f.writeSignedFloatLiteral(element)
- case *ast.SpecialFloatLiteralNode:
- f.writeSpecialFloatLiteral(element)
- case *ast.StringLiteralNode:
- f.writeStringLiteral(element)
- case *ast.SyntaxNode:
- f.writeSyntax(element)
- case *ast.UintLiteralNode:
- f.writeUintLiteral(element)
- case *ast.EmptyDeclNode:
- // Nothing to do here.
- default:
- f.err = errors.Join(f.err, fmt.Errorf("unexpected node: %T", node))
- }
-}
-
-// writeStart writes the node across as the start of a line.
-// Start nodes have their leading comments written across
-// multiple lines, but their trailing comments must be written
-// in-line to preserve the line structure.
-//
-// For example,
-//
-// // Leading comment on 'message'.
-// // Spread across multiple lines.
-// message /* This is a trailing comment on 'message' */ Foo {}
-//
-// Newlines are preserved, so that any logical grouping of elements
-// is maintained in the formatted result.
-//
-// For example,
-//
-// // Type represents a set of different types.
-// enum Type {
-// // Unspecified is the naming convention for default enum values.
-// TYPE_UNSPECIFIED = 0;
-//
-// // The following elements are the real values.
-// TYPE_ONE = 1;
-// TYPE_TWO = 2;
-// }
-//
-// Start nodes are always indented according to the formatter's
-// current level of indentation (e.g. nested messages, fields, etc).
-//
-// Note that this is one of the most complex component of the formatter - it
-// controls how each node should be separated from one another and preserves
-// newlines in the original source.
-func (f *formatter) writeStart(node ast.Node, ignoreLeadingWhitespace bool) {
- f.writeStartMaybeCompact(node, false, ignoreLeadingWhitespace)
-}
-
-func (f *formatter) writeStartMaybeCompact(
- node ast.Node,
- forceCompact bool,
- ignoreLeadingWhitespace bool,
-) {
- defer f.SetPreviousNode(node)
- info := f.nodeInfo(node)
- var (
- nodeNewlineCount = newlineCount(info.LeadingWhitespace())
- compact = forceCompact || isOpenBrace(f.previousNode)
- )
- if ignoreLeadingWhitespace {
- // If we are asked to ignore leading whitespace, then we forcibly set nodeNewlineCount
- // to 0, effectively ignoring the leading whitespace line count.
- nodeNewlineCount = 0
- }
- if length := info.LeadingComments().Len(); length > 0 {
- // If leading comments are defined, the whitespace we care about
- // is attached to the first comment.
- f.writeMultilineCommentsMaybeCompact(info.LeadingComments(), forceCompact)
- if !forceCompact && nodeNewlineCount > 1 {
- // At this point, we're looking at the lines between
- // a comment and the node its attached to.
- //
- // If the last comment is a standard comment, a single newline
- // character is sufficient to warrant a separation of the
- // two.
- //
- // If the last comment is a C-style comment, multiple newline
- // characters are required because C-style comments don't consume
- // a newline.
- f.P("")
- }
- } else if !compact && nodeNewlineCount > 1 {
- // If the previous node is an open brace, this is the first element
- // in the body of a composite type, so we don't want to write a
- // newline. This makes it so that trailing newlines are removed.
- //
- // For example,
- //
- // message Foo {
- //
- // string bar = 1;
- // }
- //
- // Is formatted into the following:
- //
- // message Foo {
- // string bar = 1;
- // }
- f.P("")
- }
- f.Indent(node)
- f.writeNode(node)
- if info.TrailingComments().Len() > 0 {
- f.writeInlineComments(info.TrailingComments())
- }
-}
-
-// writeInline writes the node and its surrounding comments in-line.
-//
-// This is useful for writing individual nodes like keywords, runes,
-// string literals, etc.
-//
-// For example,
-//
-// // This is a leading comment on the syntax keyword.
-// syntax = /* This is a leading comment on 'proto3' */" proto3";
-func (f *formatter) writeInline(node ast.Node) {
- f.inline = true
- defer func() {
- f.inline = false
- }()
- if _, ok := node.(ast.CompositeNode); ok {
- // We only want to write comments for terminal nodes.
- // Otherwise comments accessible from CompositeNodes
- // will be written twice.
- f.writeNode(node)
- return
- }
- defer f.SetPreviousNode(node)
- info := f.nodeInfo(node)
- if info.LeadingComments().Len() > 0 {
- f.writeInlineComments(info.LeadingComments())
- if info.LeadingWhitespace() != "" {
- f.Space()
- }
- }
- f.writeNode(node)
- f.writeInlineComments(info.TrailingComments())
-}
-
-// writeBodyEnd writes the node as the end of a body.
-// Leading comments are written above the token across
-// multiple lines, whereas the trailing comments are
-// written in-line and preserve their format.
-//
-// Body end nodes are always indented according to the
-// formatter's current level of indentation (e.g. nested
-// messages).
-//
-// This is useful for writing a node that concludes a
-// composite node: ']', '}', '>', etc.
-//
-// For example,
-//
-// message Foo {
-// string bar = 1;
-// // Leading comment on '}'.
-// } // Trailing comment on '}.
-func (f *formatter) writeBodyEnd(node ast.Node, leadingEndline bool) {
- if _, ok := node.(ast.CompositeNode); ok {
- // We only want to write comments for terminal nodes.
- // Otherwise comments accessible from CompositeNodes
- // will be written twice.
- f.writeNode(node)
- if f.lastWritten != '\n' {
- f.P("")
- }
- return
- }
- defer f.SetPreviousNode(node)
- info := f.nodeInfo(node)
- if leadingEndline {
- if info.LeadingComments().Len() > 0 {
- f.writeInlineComments(info.LeadingComments())
- if info.LeadingWhitespace() != "" {
- f.Space()
- }
- }
- } else {
- f.writeMultilineComments(info.LeadingComments())
- f.Indent(node)
- }
- f.writeNode(node)
- f.writeTrailingEndComments(info.TrailingComments())
-}
-
-func (f *formatter) writeLineElement(node ast.Node) {
- f.writeBodyEnd(node, false)
-}
-
-// writeBodyEndInline writes the node as the end of a body.
-// Leading comments are written above the token across
-// multiple lines, whereas the trailing comments are
-// written in-line and adapt their comment style if they
-// exist.
-//
-// Body end nodes are always indented according to the
-// formatter's current level of indentation (e.g. nested
-// messages).
-//
-// This is useful for writing a node that concludes either
-// compact options or an array literal.
-//
-// This is behaviorally similar to f.writeStart, but it ignores
-// the preceding newline logic because these body ends should
-// always be compact.
-//
-// For example,
-//
-// message Foo {
-// string bar = 1 [
-// deprecated = true
-//
-// // Leading comment on ']'.
-// ] /* Trailing comment on ']' */ ;
-// }
-func (f *formatter) writeBodyEndInline(node ast.Node, leadingInline bool) {
- if _, ok := node.(ast.CompositeNode); ok {
- // We only want to write comments for terminal nodes.
- // Otherwise comments accessible from CompositeNodes
- // will be written twice.
- f.writeNode(node)
- return
- }
- defer f.SetPreviousNode(node)
- info := f.nodeInfo(node)
- if leadingInline {
- if info.LeadingComments().Len() > 0 {
- f.writeInlineComments(info.LeadingComments())
- if info.LeadingWhitespace() != "" {
- f.Space()
- }
- }
- } else {
- f.writeMultilineComments(info.LeadingComments())
- f.Indent(node)
- }
- f.writeNode(node)
- if info.TrailingComments().Len() > 0 {
- f.writeInlineComments(info.TrailingComments())
- }
-}
-
-// writeLineEnd writes the node so that it ends a line.
-//
-// This is useful for writing individual nodes like ';' and other
-// tokens that conclude the end of a single line. In this case, we
-// don't want to transform the trailing comment's from '//' to C-style
-// because it's not necessary.
-//
-// For example,
-//
-// // This is a leading comment on the syntax keyword.
-// syntax = " proto3" /* This is a leading comment on the ';'; // This is a trailing comment on the ';'.
-func (f *formatter) writeLineEnd(node ast.Node) {
- if _, ok := node.(ast.CompositeNode); ok {
- // We only want to write comments for terminal nodes.
- // Otherwise comments accessible from CompositeNodes
- // will be written twice.
- f.writeNode(node)
- if f.lastWritten != '\n' {
- f.P("")
- }
- return
- }
- defer f.SetPreviousNode(node)
- info := f.nodeInfo(node)
- if info.LeadingComments().Len() > 0 {
- f.writeInlineComments(info.LeadingComments())
- if info.LeadingWhitespace() != "" {
- f.Space()
- }
- }
- f.writeNode(node)
- f.Space()
- f.writeTrailingEndComments(info.TrailingComments())
-}
-
-// writeMultilineComments writes the given comments as a newline-delimited block.
-// This is useful for both the beginning of a type (e.g. message, field, etc), as
-// well as the trailing comments attached to the beginning of a body block (e.g.
-// '{', '[', '<', etc).
-//
-// For example,
-//
-// // This is a comment spread across
-// // multiple lines.
-// message Foo {}
-func (f *formatter) writeMultilineComments(comments ast.Comments) {
- f.writeMultilineCommentsMaybeCompact(comments, false)
-}
-
-func (f *formatter) writeMultilineCommentsMaybeCompact(comments ast.Comments, forceCompact bool) {
- compact := forceCompact || isOpenBrace(f.previousNode)
- for i := range comments.Len() {
- comment := comments.Index(i)
- if !compact && newlineCount(comment.LeadingWhitespace()) > 1 {
- // Newlines between blocks of comments should be preserved.
- //
- // For example,
- //
- // // This is a license header
- // // spread across multiple lines.
- //
- // // Package pet.v1 defines a PetStore API.
- // package pet.v1;
- //
- f.P("")
- }
- compact = false
- f.writeComment(comment.RawText())
- f.WriteString("\n")
- }
-}
-
-// writeInlineComments writes the given comments in-line. Standard comments are
-// transformed to C-style comments so that we can safely write the comment in-line.
-//
-// Nearly all of these comments will already be C-style comments. The only cases we're
-// preventing are when the type is defined across multiple lines.
-//
-// For example, given the following:
-//
-// extend . google. // in-line comment
-// protobuf .
-// ExtensionRangeOptions {
-// optional string label = 20000;
-// }
-//
-// The formatted result is shown below:
-//
-// extend .google.protobuf./* in-line comment */ExtensionRangeOptions {
-// optional string label = 20000;
-// }
-func (f *formatter) writeInlineComments(comments ast.Comments) {
- for i := range comments.Len() {
- if i > 0 || comments.Index(i).LeadingWhitespace() != "" || f.lastWritten == ';' || f.lastWritten == '}' {
- f.Space()
- }
- text := comments.Index(i).RawText()
- if after, ok := strings.CutPrefix(text, "//"); ok {
- text = strings.TrimSpace(after)
- text = "/* " + text + " */"
- } else {
- // no multi-line comments
- lines := strings.Split(text, "\n")
- for i := range lines {
- lines[i] = strings.TrimSpace(lines[i])
- }
- text = strings.Join(lines, " ")
- }
- f.WriteString(text)
- }
-}
-
-// writeTrailingEndComments writes the given comments at the end of a line and
-// preserves the comment style. This is useful or writing comments attached to
-// things like ';' and other tokens that conclude a type definition on a single
-// line.
-//
-// If there is a newline between this trailing comment and the previous node, the
-// comments are written immediately underneath the node on a newline.
-//
-// For example,
-//
-// enum Type {
-// TYPE_UNSPECIFIED = 0;
-// }
-// // This comment is attached to the '}'
-// // So is this one.
-func (f *formatter) writeTrailingEndComments(comments ast.Comments) {
- for i := range comments.Len() {
- comment := comments.Index(i)
- if i > 0 || comment.LeadingWhitespace() != "" {
- f.Space()
- }
- f.writeComment(comment.RawText())
- }
- f.P("")
-}
-
-func (f *formatter) writeComment(comment string) {
- if strings.HasPrefix(comment, "/*") && newlineCount(comment) > 0 {
- lines := strings.Split(comment, "\n")
- // find minimum indent, so we can make all other lines relative to that
- minIndent := -1 // sentinel that means unset
- // start at 1 because line at index zero starts with "/*", not whitespace
- var prefix string
- for i := 1; i < len(lines); i++ {
- indent, ok := computeIndent(lines[i])
- if ok && (minIndent == -1 || indent < minIndent) {
- minIndent = indent
- }
- if i > 1 && len(prefix) == 0 {
- // no shared prefix
- continue
- }
- line := strings.TrimSpace(lines[i])
- if line == "*/" {
- continue
- }
- var linePrefix string
- if len(line) > 0 && isCommentPrefix(line[0]) {
- linePrefix = line[:1]
- }
- if i == 1 {
- prefix = linePrefix
- } else if linePrefix != prefix {
- // they do not share prefix
- prefix = ""
- }
- }
- if minIndent < 0 {
- // This shouldn't be necessary.
- // But we do it just in case, to avoid possible panic
- minIndent = 0
- }
- for i, line := range lines {
- trimmedLine := strings.TrimSpace(line)
- if trimmedLine == "" || trimmedLine == "*/" || len(prefix) > 0 {
- line = trimmedLine
- } else {
- // we only trim space from the right; for the left,
- // we unindent based on indentation found above.
- line = unindent(line, minIndent)
- line = strings.TrimRightFunc(line, unicode.IsSpace)
- }
- // If we have a block comment with no prefix, we'll format
- // like so:
-
- /*
- This is a multi-line comment example.
- It has no comment prefix on each line.
- */
-
- // But if there IS a prefix, "|" for example, we'll left-align
- // the prefix symbol under the asterisk of the comment start
- // like this:
-
- /*
- | This comment has a prefix before each line.
- | Usually the prefix is asterisk, but it's a
- | pipe in this example.
- */
-
- // Finally, if the comment prefix is an asterisk, we'll left-align
- // the comment end so its asterisk also aligns, like so:
-
- /*
- * This comment has a prefix before each line.
- * Usually the prefix is asterisk, which is the
- * case in this example.
- */
-
- if i > 0 && line != "*/" {
- if len(prefix) == 0 {
- line = " " + line
- } else {
- line = " " + line
- }
- }
- if line == "*/" && prefix == "*" {
- // align the comment end with the other asterisks
- line = " " + line
- }
-
- if i != len(lines)-1 {
- f.P(line)
- } else {
- // for last line, we don't use P because we don't
- // want to print a trailing newline
- f.Indent(nil)
- f.WriteString(line)
- }
- }
- } else {
- f.Indent(nil)
- f.WriteString(strings.TrimSpace(comment))
- }
-}
-
-func isCommentPrefix(ch byte) bool {
- r := rune(ch)
- // A multi-line comment prefix is *usually* an asterisk, like in the following
- /*
- * Foo
- * Bar
- * Baz
- */
- // But we'll allow other prefixes. But if it's a letter or number, it's not a prefix.
- return !unicode.IsLetter(r) && !unicode.IsNumber(r)
-}
-
-func unindent(s string, unindent int) string {
- pos := 0
- for i, r := range s {
- if pos == unindent {
- return s[i:]
- }
- if pos > unindent {
- // removing tab-stop unindented too far, so we
- // add back some spaces to compensate
- return strings.Repeat(" ", pos-unindent) + s[i:]
- }
-
- switch r {
- case ' ':
- pos++
- case '\t':
- // jump to next tab stop
- pos += 8 - (pos % 8)
- default:
- return s[i:]
- }
- }
- // nothing but whitespace...
- return ""
-}
-
-func computeIndent(s string) (int, bool) {
- if strings.TrimSpace(s) == "*/" {
- return 0, false
- }
- indent := 0
- for _, r := range s {
- switch r {
- case ' ':
- indent++
- case '\t':
- // jump to next tab stop
- indent += 8 - (indent % 8)
- default:
- return indent, true
- }
- }
- // if we get here, line is nothing but whitespace
- return 0, false
-}
-
-func (f *formatter) leadingCommentsContainBlankLine(n ast.Node) bool {
- info := f.nodeInfo(n)
- comments := info.LeadingComments()
- for i := range comments.Len() {
- if newlineCount(comments.Index(i).LeadingWhitespace()) > 1 {
- return true
- }
- }
- return newlineCount(info.LeadingWhitespace()) > 1
-}
-
-func (f *formatter) importHasComment(importNode *ast.ImportNode) bool {
- if f.nodeHasComment(importNode) {
- return true
- }
- if importNode == nil {
- return false
- }
-
- return f.nodeHasComment(importNode.Keyword) ||
- f.nodeHasComment(importNode.Name) ||
- f.nodeHasComment(importNode.Semicolon) ||
- f.nodeHasComment(importNode.Modifier)
-}
-
-func (f *formatter) nodeHasComment(node ast.Node) bool {
- // when node != nil, node's value could be nil, see: https://go.dev/doc/faq#nil_error
- if node == nil || reflect.ValueOf(node).IsNil() {
- return false
- }
-
- nodeinfo := f.nodeInfo(node)
- return nodeinfo.LeadingComments().Len() > 0 ||
- nodeinfo.TrailingComments().Len() > 0
-}
-
-func (f *formatter) setTrailingComments(node ast.Node, comments ast.Comments) {
- f.overrideTrailingComments[node] = comments
-}
-
-func (f *formatter) nodeInfo(node ast.Node) nodeInfo {
- info := f.fileNode.NodeInfo(node)
- if trailingComments, ok := f.overrideTrailingComments[node]; ok {
- return infoWithTrailingComments{info, trailingComments}
- }
- return info
-}
-
-type nodeInfo interface {
- Start() ast.SourcePos
- End() ast.SourcePos
- LeadingComments() ast.Comments
- TrailingComments() ast.Comments
- LeadingWhitespace() string
- RawText() string
-}
-
-type infoWithTrailingComments struct {
- ast.NodeInfo
- trailing ast.Comments
-}
-
-func (n infoWithTrailingComments) TrailingComments() ast.Comments {
- return n.trailing
-}
-
-// importSortOrder maps import types to a sort order number, so it can be compared and sorted.
-// Higher values sort first: `import`=3, `import public`=2, `import weak`=1.
-func importSortOrder(node *ast.ImportNode) int {
- if node.Modifier != nil {
- switch node.Modifier.Val {
- case "public":
- return 2
- case "weak":
- return 1
- }
- }
- return 3
-}
-
-// isOptionImport reports whether the import has the "option" modifier.
-func isOptionImport(node *ast.ImportNode) bool {
- return node.Modifier != nil && node.Modifier.Val == "option"
-}
-
-// stringForOptionName returns the string representation of the given option name node.
-// This is used for sorting file-level options.
-func stringForOptionName(optionNameNode *ast.OptionNameNode) string {
- var result strings.Builder
- for j, part := range optionNameNode.Parts {
- if j > 0 {
- // Add a dot between each of the parts.
- result.WriteString(".")
- }
- result.WriteString(stringForFieldReference(part))
- }
- return result.String()
-}
-
-// stringForFieldReference returns the string representation of the given field reference.
-// This is used for sorting file-level options.
-func stringForFieldReference(fieldReference *ast.FieldReferenceNode) string {
- var result string
- if fieldReference.Open != nil {
- result += "("
- }
- result += string(fieldReference.Name.AsIdentifier())
- if fieldReference.Close != nil {
- result += ")"
- }
- return result
-}
-
-// isOpenBrace returns true if the given node represents one of the
-// possible open brace tokens, namely '{', '[', or '<'.
-func isOpenBrace(node ast.Node) bool {
- if node == nil {
- return false
- }
- runeNode, ok := node.(*ast.RuneNode)
- if !ok {
- return false
- }
- return runeNode.Rune == '{' || runeNode.Rune == '[' || runeNode.Rune == '<'
-}
-
-// newlineCount returns the number of newlines in the given value.
-// This is useful for determining whether or not we should preserve
-// the newline between nodes.
-//
-// The newlines don't need to be adjacent to each other - all of the
-// tokens between them are other whitespace characters, so we can
-// safely ignore them.
-func newlineCount(value string) int {
- return strings.Count(value, "\n")
-}
-
-func messageLiteralOpen(msg *ast.MessageLiteralNode) *ast.RuneNode {
- node := msg.Open
- if node.Rune == '{' {
- return node
- }
- // If it's not "{" then this message literal used "<" and ">" to enclose it.
- // For consistent formatted output, change it to "{".
- return ast.NewRuneNode('{', node.Token())
-}
-
-func messageLiteralClose(msg *ast.MessageLiteralNode) *ast.RuneNode {
- node := msg.Close
- if node.Rune == '}' {
- return node
- }
- // If it's not "}" then this message literal used "<" and ">" to enclose it.
- // For consistent formatted output, change it to "}".
- return ast.NewRuneNode('}', node.Token())
-}
-
-// writeDeprecatedOption writes "option deprecated = true;" on its own line.
-// This is used to inject deprecation options for types that should be deprecated.
-func (f *formatter) writeDeprecatedOption() {
- f.Indent(nil)
- f.WriteString("option deprecated = true;")
- f.P("")
-}
-
-// currentFQN returns the current fully-qualified name.
-func (f *formatter) currentFQN() string {
- return f.packageFQN
-}
-
-// pushFQN appends a name component to the current FQN and returns a function to restore it.
-func (f *formatter) pushFQN(name string) func() {
- original := f.packageFQN
- if f.packageFQN == "" {
- f.packageFQN = name
- } else {
- f.packageFQN = f.packageFQN + "." + name
- }
- return func() {
- f.packageFQN = original
- }
-}
-
-// shouldInjectDeprecation returns true if the given FQN should have a deprecated option injected.
-// It also sets deprecationMatched to true if the FQN matches the prefix.
-func (f *formatter) shouldInjectDeprecation(fqn string) bool {
- if f.deprecation == nil {
- return false
- }
- if f.deprecation.matchesPrefix(fqn) {
- f.deprecationMatched = true
- return true
- }
- return false
-}
-
-// shouldInjectDeprecationExact returns true if the given FQN should have a deprecated option
-// injected using exact matching (for fields and enum values).
-// It also sets deprecationMatched to true if the FQN matches exactly.
-func (f *formatter) shouldInjectDeprecationExact(fqn string) bool {
- if f.deprecation == nil {
- return false
- }
- if f.deprecation.matchesExact(fqn) {
- f.deprecationMatched = true
- return true
- }
- return false
-}
-
-// writeCompactDeprecatedOption writes " [deprecated = true]" for compact options.
-func (f *formatter) writeCompactDeprecatedOption() {
- f.WriteString(" [deprecated = true]")
-}
diff --git a/private/buf/bufformat/testdata/customoptions/options.golden b/private/buf/bufformat/testdata/customoptions/options.golden
index fe7593571a..0477425326 100644
--- a/private/buf/bufformat/testdata/customoptions/options.golden
+++ b/private/buf/bufformat/testdata/customoptions/options.golden
@@ -70,7 +70,6 @@ extend google.protobuf.EnumValueOptions {
bool enum_value_option = 80015;
Thing enum_value_thing_option = 80016;
}
-
// this is a comment before service options
extend google.protobuf.ServiceOptions {
bool service_option = 80012;
diff --git a/private/buf/bufformat/testdata/proto3/all/v1/all.golden b/private/buf/bufformat/testdata/proto3/all/v1/all.golden
index a7a0e62e64..1e8de7df67 100644
--- a/private/buf/bufformat/testdata/proto3/all/v1/all.golden
+++ b/private/buf/bufformat/testdata/proto3/all/v1/all.golden
@@ -8,18 +8,18 @@ syntax = "proto3"; // syntax-level-inline comment
package all.v1; // package-level-inline comment
+// between-package-and-import comment
+
import "custom.proto";
// between-imports comment
import "google/protobuf/duration.proto"; // import-inline comment 2
-// between-package-and-import comment
-
// import comment
import "google/protobuf/timestamp.proto"; // import-inline comment
-// option comment 3
-option go_package = "foopb"; // option-inline comment 3
// between-imports-and-options comment
+// option comment 3
+option go_package = "foopb"; // option-inline comment 3
// option comment
option java_multiple_files = true; // option-inline comment
// option comment 4
diff --git a/private/buf/bufformat/testdata/proto3/service/v1/service.golden b/private/buf/bufformat/testdata/proto3/service/v1/service.golden
index 9a23463ce9..28c56eecae 100644
--- a/private/buf/bufformat/testdata/proto3/service/v1/service.golden
+++ b/private/buf/bufformat/testdata/proto3/service/v1/service.golden
@@ -7,7 +7,7 @@ service /* Before Ping comment */ Ping /* After Ping comment */ { // Trailing co
// This service is deprecated.
option deprecated = true; // In-line comment on deprecated option.
- rpc /* Before Ping Method */ Ping(/* Before Request */Message/* After Request */) returns /* Before paren */ (Message);
+ rpc /* Before Ping Method */ Ping(/* Before Request */Message /* After Request */) returns /* Before paren */ (Message);
rpc Echo(Message) returns (Message) { // Trailing comment on '{'
option deprecated = true; // In-line on deprecated option.
@@ -16,5 +16,5 @@ service /* Before Ping comment */ Ping /* After Ping comment */ { // Trailing co
}
// The Streamer method is bidirectional.
- rpc Streamer(/* Client-side streaming */ stream Message) returns (stream /* Server-side streaming */ Message);
+ rpc Streamer( /* Client-side streaming */ stream Message) returns (stream /* Server-side streaming */ Message);
}
diff --git a/private/buf/buflsp/server.go b/private/buf/buflsp/server.go
index 1bb10385ff..b17599c22f 100644
--- a/private/buf/buflsp/server.go
+++ b/private/buf/buflsp/server.go
@@ -25,8 +25,9 @@ import (
celpv "buf.build/go/protovalidate/cel"
"buf.build/go/standard/xslices"
"github.com/bufbuild/buf/private/buf/bufformat"
- "github.com/bufbuild/protocompile/parser"
- "github.com/bufbuild/protocompile/reporter"
+ "github.com/bufbuild/protocompile/experimental/parser"
+ "github.com/bufbuild/protocompile/experimental/report"
+ "github.com/bufbuild/protocompile/experimental/source"
"github.com/google/cel-go/cel"
"go.lsp.dev/protocol"
"mvdan.cc/xurls/v2"
@@ -321,12 +322,19 @@ func (s *server) Formatting(
// Format for a file we don't know about? Seems bad!
return nil, fmt.Errorf("received update for file that was not open: %q", params.TextDocument.URI)
}
- parsed, err := parser.Parse(file.uri.Filename(), strings.NewReader(file.file.Text()), reporter.NewHandler(nil))
- if err != nil {
- return nil, fmt.Errorf("cannot format file: %w", err)
+ // We only use the diagnostic count to decide whether to fail, so suppress
+ // warnings at the source rather than generating them just to ignore.
+ r := &report.Report{Options: report.Options{SuppressWarnings: true}}
+ parsed, ok := parser.Parse(
+ file.uri.Filename(),
+ source.NewFile(file.uri.Filename(), file.file.Text()),
+ r,
+ )
+ if !ok {
+ return nil, fmt.Errorf("cannot format file: %d parse error(s)", len(r.Diagnostics))
}
var out strings.Builder
- if err := bufformat.FormatFileNode(&out, parsed); err != nil {
+ if err := bufformat.FormatFile(&out, parsed); err != nil {
return nil, fmt.Errorf("format failed: %w", err)
}
newText := out.String()