From 06edc5bf9cb9d0088986737419b5975d75fd957c Mon Sep 17 00:00:00 2001 From: Alex Abashev Date: Fri, 30 Oct 2026 13:32:34 +0300 Subject: [PATCH] Indent a comment after an opening delimiter like the code after it 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. --- .../com/palantir/javaformat/OpsBuilder.java | 25 ++++++++++- .../javaformat/java/JavaInputAstVisitor.java | 6 +++ .../commentAfterOpeningDelimiter-assert.case | 35 ++++++++++++++++ ...ntAfterOpeningDelimiter-enumSemicolon.case | 41 +++++++++++++++++++ ...ommentAfterOpeningDelimiter-forHeader.case | 34 +++++++++++++++ ...mmentAfterOpeningDelimiter-multiCatch.case | 35 ++++++++++++++++ ...entAfterOpeningDelimiter-tryResources.case | 31 ++++++++++++++ ...ntAfterOpeningDelimiter-typeArguments.case | 41 +++++++++++++++++++ 8 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-assert.case create mode 100644 open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-enumSemicolon.case create mode 100644 open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-forHeader.case create mode 100644 open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-multiCatch.case create mode 100644 open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-tryResources.case create mode 100644 open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-typeArguments.case diff --git a/open-java-format/src/main/java/com/palantir/javaformat/OpsBuilder.java b/open-java-format/src/main/java/com/palantir/javaformat/OpsBuilder.java index c8bf51632..7ff81101e 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/OpsBuilder.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/OpsBuilder.java @@ -309,6 +309,26 @@ public void close() { add(CloseOp.make()); } + /** + * Keep the comments before the next token inside the level just opened. {@link #build} puts the comments before a + * token ahead of the {@link OpenOp}s right before it, so that a comment before a statement or an expression stays + * out of their levels. Right after an opening delimiter, such as the {@code (} of a for loop, that takes the + * comment, and the line break after it, out of the level that indents what follows the delimiter. + */ + public void fenceComments() { + add(CommentFence.FENCE); + } + + /** Where {@link #build} stops looking back over {@link OpenOp}s for the place of a token's comments. */ + private enum CommentFence implements Op { + FENCE; + + @Override + public void add(DocBuilder builder) { + throw new IllegalStateException("build() drops comment fences"); + } + } + /** Return the text of the next {@link Input.Token}, or absent if there is none. */ public Optional peekToken() { return peekToken(0); @@ -523,7 +543,7 @@ public OpsOutput build() { * when possible. */ Input.Token token = tokenOp.getToken(); - int j = i; // Where to insert toksBefore before. + int j = i; // Where to insert toksBefore before: ahead of the OpenOps before the token, up to a fence. while (0 < j && ops.get(j - 1) instanceof OpenOp) { --j; } @@ -669,6 +689,9 @@ public OpsOutput build() { } } Op op = ops.get(i); + if (op == CommentFence.FENCE) { + continue; + } if (afterForcedBreak && (op instanceof NonBreakingSpace || (op instanceof Break b diff --git a/open-java-format/src/main/java/com/palantir/javaformat/java/JavaInputAstVisitor.java b/open-java-format/src/main/java/com/palantir/javaformat/java/JavaInputAstVisitor.java index 6596c4052..ad0f8df96 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/java/JavaInputAstVisitor.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/java/JavaInputAstVisitor.java @@ -605,6 +605,7 @@ public Void visitAssert(AssertTree node, Void unused) { token("assert"); builder.space(); builder.open(node.getDetail() == null ? ZERO : plusFour); + builder.fenceComments(); scan(node.getCondition(), null); if (node.getDetail() != null) { builder.breakOp(" "); @@ -912,6 +913,7 @@ public boolean visitEnumDeclaration(ClassTree node) { builder.close(); if (builder.peekToken().equals(Optional.of(";"))) { builder.open(plusTwo); + builder.fenceComments(); token(";"); builder.forcedBreak(); dropEmptyDeclarations(); @@ -1010,6 +1012,7 @@ public Void visitForLoop(ForLoopTree node, Void unused) { builder.space(); token("("); builder.open(plusFour); + builder.fenceComments(); builder.open( node.getInitializer().size() > 1 && node.getInitializer().get(0) instanceof ExpressionStatementTree ? plusFour @@ -1942,6 +1945,7 @@ public Void visitTry(TryTree node, Void unused) { if (!node.getResources().isEmpty()) { token("("); builder.open(node.getResources().size() > 1 ? plusFour : ZERO); + builder.fenceComments(); boolean first = true; for (Tree resource : node.getResources()) { if (!first) { @@ -2382,6 +2386,7 @@ private void visitCatchClause(CatchTree node, AllowTrailingBlankLine allowTraili builder.space(); token("("); builder.open(plusFour); + builder.fenceComments(); VariableTree ex = node.getParameter(); if (ex.getType() instanceof UnionTypeTree) { builder.open(ZERO); @@ -3179,6 +3184,7 @@ void addTypeArguments(List typeArguments, Indent plusIndent) { } token("<"); builder.open(plusIndent); + builder.fenceComments(); boolean first = true; for (Tree typeArgument : typeArguments) { if (!first) { diff --git a/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-assert.case b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-assert.case new file mode 100644 index 000000000..45846092b --- /dev/null +++ b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-assert.case @@ -0,0 +1,35 @@ +# A comment on its own line right after `assert` stays inside the condition at the continuation indent, together +# with the code after it, when the assertion has a detail message (google-java-format#1501, #171). +---------- +class T { + void f(int x, int y) { + assert + // comment + x > 0 : "positive"; + assert + // comment + x > 0 : "positive"; + assert + /* why */ + x < + y : + "x below " + "y"; + } +} +---------- +class T { + void f(int x, int y) { + assert + // comment + x > 0 + : "positive"; + assert + // comment + x > 0 + : "positive"; + assert + /* why */ + x < y + : "x below " + "y"; + } +} diff --git a/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-enumSemicolon.case b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-enumSemicolon.case new file mode 100644 index 000000000..1b57d59ac --- /dev/null +++ b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-enumSemicolon.case @@ -0,0 +1,41 @@ +# A comment on its own line before the `;` that ends the constants of an enum stays at the indent of the constants, +# and so does the `;` (google-java-format#1501, #171). +---------- +enum E1 { + A, + // comment + ; +} + +enum E2 { + A, B +// comment +; + void f() {} +} + +enum E3 { A + /* comment */ + ; +} +---------- +enum E1 { + A, + // comment + ; +} + +enum E2 { + A, + B + // comment + ; + + void f() {} +} + +enum E3 { + A + /* comment */ + ; +} diff --git a/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-forHeader.case b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-forHeader.case new file mode 100644 index 000000000..ac525340f --- /dev/null +++ b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-forHeader.case @@ -0,0 +1,34 @@ +# A comment on its own line right after the `(` of a for loop stays inside the header at the continuation indent, +# which breaks the header, so the condition and the update get a line each (google-java-format#1501, #171). A block +# comment that shares its line with the initializer stays with it, on the line of the header when that fits. +---------- +class T { + void f(int n) { + for ( + // comment + int i = 0; i < n; i++) {} + for ( + /* comment */ + int i = 0; i < n; i++) {} + for ( + /* c */ int i = 0; + i < n; + i++) {} + } +} +---------- +class T { + void f(int n) { + for ( + // comment + int i = 0; + i < n; + i++) {} + for ( + /* comment */ + int i = 0; + i < n; + i++) {} + for (/* c */ int i = 0; i < n; i++) {} + } +} diff --git a/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-multiCatch.case b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-multiCatch.case new file mode 100644 index 000000000..e37cc940a --- /dev/null +++ b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-multiCatch.case @@ -0,0 +1,35 @@ +# A comment on its own line right after the `(` of a multi-catch clause stays inside the clause at the continuation +# indent, with the caught types after it (google-java-format#1501, #171). +---------- +class T { + void f() { + try { + g(); + } catch ( + // comment + IllegalArgumentException | IllegalStateException e) { + } + try { g(); } catch ( + /* comment */ + IllegalArgumentException + | IllegalStateException e) { h(e); } + } +} +---------- +class T { + void f() { + try { + g(); + } catch ( + // comment + IllegalArgumentException | IllegalStateException e) { + } + try { + g(); + } catch ( + /* comment */ + IllegalArgumentException | IllegalStateException e) { + h(e); + } + } +} diff --git a/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-tryResources.case b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-tryResources.case new file mode 100644 index 000000000..a686821d8 --- /dev/null +++ b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-tryResources.case @@ -0,0 +1,31 @@ +# A comment on its own line right after the `(` of a try with several resources stays inside the resources at the +# continuation indent, with the first resource after it (google-java-format#1501, #171). +---------- +class T { + void f(AutoCloseable a, AutoCloseable b) throws Exception { + try ( + // comment + AutoCloseable r1 = a; AutoCloseable r2 = b) {} + try ( + /* comment */ + AutoCloseable r1 = a; + AutoCloseable r2 = b) { + use(r1, r2); + } + } +} +---------- +class T { + void f(AutoCloseable a, AutoCloseable b) throws Exception { + try ( + // comment + AutoCloseable r1 = a; + AutoCloseable r2 = b) {} + try ( + /* comment */ + AutoCloseable r1 = a; + AutoCloseable r2 = b) { + use(r1, r2); + } + } +} diff --git a/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-typeArguments.case b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-typeArguments.case new file mode 100644 index 000000000..75f89119f --- /dev/null +++ b/open-java-format/src/test/resources/test-cases/commentAfterOpeningDelimiter-typeArguments.case @@ -0,0 +1,41 @@ +# A comment on its own line right after the `<` of explicit type arguments stays inside them at the continuation +# indent, with the type after it, in a constructor call and in a method reference (google-java-format#1501, #171). +---------- +class T { + T() {} + + static void g() {} + + void f() { + new < + // comment + String>T(); + Runnable r = + T + ::< + // comment + String>g; + new < + /* comment */ + String> T(); + } +} +---------- +class T { + T() {} + + static void g() {} + + void f() { + new < + // comment + String>T(); + Runnable r = T + ::< + // comment + String>g; + new < + /* comment */ + String>T(); + } +}