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()