Skip to content

buf format silently deletes a comment that is separated from the following element by a blank line inside an option aggregate (not idempotent) #4620

Description

@garrettr

buf version: 1.72.0 (also reproduces on 1.71.0; 1.70.0 is unaffected). Built from source at tag v1.72.0, linux/amd64, Go 1.26.4.

Command: buf format repro.proto, applied repeatedly to its own output.

Repro file (repro.proto)

syntax = "proto3";

package example.v1;

import "google/protobuf/descriptor.proto";

message CallerEntry {
  string service_account = 1;
}

message AclRule {
  repeated CallerEntry callers = 1;
}

extend google.protobuf.MethodOptions {
  AclRule acl = 50001;
}

message Req {}
message Resp {}

service ExampleService {
  rpc GetThing(Req) returns (Resp) {
    option (example.v1.acl) = {
      callers: [
        {service_account: "beta"},
        /* detached comment */

        {service_account: "gamma"}
      ]
    };
  }
}

Observed

Each pass produces different output, and the comment is destroyed on the second pass.

Pass 1 — the comment moves past the element it annotated, down to the closing bracket, and is dedented to the bracket's level:

      callers: [
        {service_account: "beta"},
        {service_account: "gamma"}
      /* detached comment */

      ]

Pass 2 — the comment is gone from the output entirely:

      callers: [
        {service_account: "beta"},
        {service_account: "gamma"}
      ]

Pass 3 and later are identical to pass 2, so the file settles into a stable state with the comment permanently lost.

Expected

format(format(x)) == format(x), and no comment content is ever dropped.

Notes

  • The trigger is the blank line between the comment and the element that follows it. Deleting the blank line from the repro makes the file stable after pass 1 with the comment preserved in place.
  • Comment style does not matter: a // detached comment in the same position is deleted the same way.
  • The comment drifts one element per pass before being deleted, so a longer list delays the loss rather than avoiding it. With three following entries the deletion lands on pass 3 instead of pass 2, which makes it easy to miss — a single format run looks merely like an odd reflow, and the content disappears on a later, unrelated run.
  • Practical impact: buf format -w in a pre-commit hook. The first commit that touches such a file moves the comment, a later commit deletes it, and neither diff is obviously a content loss to a reviewer.
  • 1.70.0 does not lose the comment. It joins it onto the following element instead (/* detached comment */ {service_account: "gamma"}), which is stable under repeated formatting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions