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(); + } +}