Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## [Unreleased]

- No changes yet.
- Add support for Edition 2024 syntax to `buf format`.

## [v1.66.0] - 2026-02-23

Expand Down
27 changes: 22 additions & 5 deletions private/buf/bufformat/bufformat.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,28 @@ func formatFileNode(dest io.Writer, fileNode *ast.FileNode, options *formatOptio

// 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. This will
// capture unknown syntax like edition "2024" which at the current time is
// not supported.
if _, err := parser.ResultFromAST(fileNode, true, reporter.NewHandler(nil)); err != nil {
return false, err
// 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
}
}
formatter := newFormatter(dest, fileNode, options)
if err := formatter.Run(); err != nil {
Expand Down
52 changes: 36 additions & 16 deletions private/buf/bufformat/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,21 +292,21 @@ func (f *formatter) writeFileHeader() {
sort.Slice(importNodes, func(i, j int) bool {
iName := importNodes[i].Name.AsString()
jName := importNodes[j].Name.AsString()
// sort by public > None > weak
iOrder := importSortOrder(importNodes[i])
jOrder := importSortOrder(importNodes[j])

if iName < jName {
return true
}
if iName > jName {
return false
// "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 iOrder > jOrder {
return true
if iName != jName {
return iName < jName
}
if iOrder < jOrder {
return false
iOrder := importSortOrder(importNodes[i])
jOrder := importSortOrder(importNodes[j])
if iOrder != jOrder {
return iOrder < jOrder
}

// put commented import first
Expand Down Expand Up @@ -447,6 +447,9 @@ func (f *formatter) writeImport(importNode *ast.ImportNode, forceCompact, first
case importNode.Weak != nil:
f.writeInline(importNode.Weak)
f.Space()
case importNode.Modifier != nil:
f.writeInline(importNode.Modifier)
f.Space()
}
f.writeInline(importNode.Name)
f.writeLineEnd(importNode.Semicolon)
Expand Down Expand Up @@ -626,7 +629,13 @@ func (f *formatter) writeMessage(messageNode *ast.MessageNode) {
f.writeDeprecatedOption()
}
}
f.writeStart(messageNode.Keyword, false)
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()
Expand Down Expand Up @@ -911,7 +920,13 @@ func (f *formatter) writeEnum(enumNode *ast.EnumNode) {
f.writeDeprecatedOption()
}
}
f.writeStart(enumNode.Keyword, false)
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()
Expand Down Expand Up @@ -2538,7 +2553,7 @@ func (n infoWithTrailingComments) TrailingComments() ast.Comments {
}

// importSortOrder maps import types to a sort order number, so it can be compared and sorted.
// `import`=3, `import public`=2, `import weak`=1
// Higher values sort first: `import`=3, `import public`=2, `import weak`=1.
func importSortOrder(node *ast.ImportNode) int {
switch {
case node.Public != nil:
Expand All @@ -2550,6 +2565,11 @@ func importSortOrder(node *ast.ImportNode) int {
}
}

// 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 {
Expand Down
16 changes: 1 addition & 15 deletions private/buf/bufformat/formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func testFormatCustomOptions(t *testing.T) {

func testFormatEditions(t *testing.T) {
testFormatNoDiff(t, "testdata/editions/2023")
testFormatError(t, "testdata/editions/2024", `edition "2024" not yet fully supported; latest supported edition "2023"`)
testFormatNoDiff(t, "testdata/editions/2024")
}

func testFormatProto2(t *testing.T) {
Expand Down Expand Up @@ -117,20 +117,6 @@ func testFormatNoDiff(t *testing.T, path string) {
})
}

func testFormatError(t *testing.T, path string, errContains string) {
t.Run(path, func(t *testing.T) {
ctx := context.Background()
bucket, err := storageos.NewProvider().NewReadWriteBucket(path)
require.NoError(t, err)
moduleSetBuilder := bufmodule.NewModuleSetBuilder(ctx, slogtestext.NewLogger(t), bufmodule.NopModuleDataProvider, bufmodule.NopCommitProvider)
moduleSetBuilder.AddLocalModule(bucket, path, true)
moduleSet, err := moduleSetBuilder.Build()
require.NoError(t, err)
_, err = FormatModuleSet(ctx, moduleSet)
require.ErrorContains(t, err, errContains)
})
}

func TestFormatterWithDeprecation(t *testing.T) {
t.Parallel()
// Test basic deprecation with prefix matching
Expand Down
4 changes: 4 additions & 0 deletions private/buf/bufformat/testdata/editions/2023/editions.golden
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ edition = "2023";

package a.b.c;

import "a.proto";
import public "b.proto";
import weak "c.proto";
import "d.proto";
import "google/protobuf/descriptor.proto";

option features.(string_feature) = "abc";
Expand Down
4 changes: 4 additions & 0 deletions private/buf/bufformat/testdata/editions/2023/editions.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ edition = "2023";
package a.b.c;

import "google/protobuf/descriptor.proto";
import "a.proto";
import "d.proto";
import weak "c.proto";
import public "b.proto";

extend google.protobuf.FeatureSet {
string string_feature = 9995;
Expand Down
38 changes: 38 additions & 0 deletions private/buf/bufformat/testdata/editions/2024/editions.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
edition = "2024";

package a.b.c;

import public "a.proto";
import "c.proto";
import option "b.proto";
import option "google/protobuf/descriptor.proto";

export message PublicApi {
string id = 1;
}

local message InternalHelper {
int32 code = 1;
}

message DefaultVisibility {
string name = 1;
export message Nested {
string field = 1;
}
}

export enum Status {
STATUS_UNSPECIFIED = 0;
OK = 1;
ERROR = 2;
}

local enum InternalCode {
INTERNAL_CODE_UNSPECIFIED = 0;
RETRY = 1;
}

enum DefaultEnum {
DEFAULT_ENUM_UNSPECIFIED = 0;
}
31 changes: 31 additions & 0 deletions private/buf/bufformat/testdata/editions/2024/editions.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,34 @@ edition = "2024";
package a.b.c;

import option "google/protobuf/descriptor.proto";
import "c.proto";
import option "b.proto";
import public "a.proto";

export message PublicApi {
string id = 1;
}

local message InternalHelper {
int32 code = 1;
}

message DefaultVisibility {
string name = 1;
export message Nested { string field = 1; }
}

export enum Status {
STATUS_UNSPECIFIED = 0;
OK = 1;
ERROR = 2;
}

local enum InternalCode {
INTERNAL_CODE_UNSPECIFIED = 0;
RETRY = 1;
}

enum DefaultEnum {
DEFAULT_ENUM_UNSPECIFIED = 0;
}