Skip to content

Indent a comment after an opening delimiter like the code after it - #175

Merged
abashev merged 1 commit into
21.X.Xfrom
fix/comment-after-opening-delimiter
Oct 30, 2026
Merged

abashev merged 1 commit into
21.X.Xfrom
fix/comment-after-opening-delimiter

Conversation

@abashev

@abashev abashev commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Fixes #171.

What happens

A comment on its own line right after an opening delimiter came out at the indent of the statement, and so did the code after it. The delimiters are for (, the ( of a try with several resources and of a multi-catch clause, the < of explicit type arguments (new <String>T(), T::<String>g), assert with a detail message, and the ; that ends the constants of an enum. For the enum, the comment and the ; even landed in the enum's own column.

Input:

class T {
  void f(int n) {
    for (
      // comment
      int i = 0; i < n; i++) {}
  }
}

enum E {
  A,
    // comment
    ;
}

Before:

class T {
    void f(int n) {
        for (
        // comment
        int i = 0; i < n; i++) {}
    }
}

enum E {
    A,
// comment
;
}

After:

class T {
    void f(int n) {
        for (
                // comment
                int i = 0;
                i < n;
                i++) {}
    }
}

enum E {
    A,
    // comment
    ;
}

Why

OpsBuilder.build places the comments before a token ahead of the OpenOps right before it, so that a comment before a statement or an expression stays out of their levels. Each of these delimiters opens the level that indents what follows it right before that level's first token. The comment therefore left the level, and the forced break after the comment took the next line out with it.

google/google-java-format#1501 fixes the same defect in google-java-format's new layout builder (groundwork in google/google-java-format#1500, baseline cases in google/google-java-format#1502). Its code does not apply to our engine, but the idea does.

The fix

  • OpsBuilder.fenceComments() adds a marker that build stops at when it looks back over OpenOps, and drops before the ops become a Doc.
  • The six places call it right after they open their level, as upstream does: visitAssert, the ; after enum constants, visitForLoop, visitTry, visitCatchClause and addTypeArguments.
  • The comment now breaks the level it sits in. The parts of a for header and the : of an assert get a line each, as they do whenever that level breaks.
  • An assert without a detail message and a try with one resource open a level without indent, so their comments stay where they were.
  • A block comment that shares its line with the code after it stays with it, on the line of the delimiter when that fits: for (/* c */ int i = 0; i < n; i++) {}.

Tests

There are six new cases, test-cases/commentAfterOpeningDelimiter-{assert,enumSemicolon,forHeader,tryResources,multiCatch,typeArguments}.case. Each input mixes upstream's shape, the shape 21.X.X produces today, and odd breaks and spacing. All six failed on the unchanged code with the comment at the statement's indent, and they pass with the fix, including the second run.

On JDK 21:

  • FormatterCasesTest: 887 → 893.
  • The module: 1408 → 1414 tests, 14 skipped.
  • ./gradlew test --rerun is green for all modules (Gradle plugin 51 tests, IntelliJ plugin 10).

JDK corpus

None of the 15,747 files of the JDK 21 sources formats differently. None of them has a comment on its own line after one of these delimiters.

A comment on its own line right after `for (`, after the `(` of a try
with several resources or of a multi-catch clause, after the `<` of
explicit type arguments or after `assert` with a detail message, or
before the `;` that ends the constants of an enum, came out at the
indent of the statement, and so did the code after it; the comment and
the `;` of an enum even landed in the enum's own column (#171):

    for (
    // comment
    int i = 0; i < n; i++) {}

OpsBuilder.build puts the comments before a token ahead of the OpenOps
right before it, so that a comment before a statement or an expression
stays out of their levels. These delimiters open the level that
indents what follows them right before its first token, so the comment
left that level, and the forced break after it took the next line out
with it.

OpsBuilder.fenceComments marks where build stops looking back, and the
six places call it right after opening their level, as
google/google-java-format#1501 does in its own layout builder
(groundwork in google/google-java-format#1500, cases in
google/google-java-format#1502). The comment now breaks the level it is
in, so the parts of a for header and the `:` of an assert get a line
each:

    for (
            // comment
            int i = 0;
            i < n;
            i++) {}

An assert without a detail message and a try with one resource open a
level without indent, so their comments stay where they were. A block
comment that shares its line with the code after it stays with it, on
the line of the delimiter when that fits.

The six cases commentAfterOpeningDelimiter-* fail without the fix. Of
the 15,747 files of the JDK 21 sources none formats differently: none
has a comment on its own line after one of these delimiters.

Tests on JDK 21: FormatterCasesTest 887 -> 893; 1414 tests in the
module.
@abashev
abashev merged commit af36054 into 21.X.X Oct 30, 2026
17 checks passed
@abashev
abashev deleted the fix/comment-after-opening-delimiter branch October 30, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Leading comment after an opening delimiter is hoisted out of its group

1 participant