Skip to content

Commit 369d3bb

Browse files
authored
Merge branch 'main' into gjf-1460-no-lookahead-regex
2 parents fa9cdfb + 825828a commit 369d3bb

6 files changed

Lines changed: 206 additions & 4 deletions

File tree

‎open-java-format/src/main/java/com/palantir/javaformat/java/JavaCommentsHelper.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ private static String lineCommentPrefix(String line) {
185185
private List<String> wrapLineComments(List<String> lines, int column0) {
186186
List<String> result = new ArrayList<>();
187187
for (String line : lines) {
188+
// From JDK 23 on, javac returns a run of `///` markdown lines as a single comment tok, so
189+
// every line after the first still carries its source indentation. indentLineComments
190+
// trims and re-indents all of them, so both the slash prefix and the width budget have to
191+
// be read from the trimmed text: otherwise the prefix comes back empty and the wrapped
192+
// remainder is emitted as bare code.
193+
line = CharMatcher.whitespace().trimLeadingFrom(line);
188194
// Add missing leading spaces to line comments: `//foo` -> `// foo`.
189195
Matcher matcher = LINE_COMMENT_MISSING_SPACE_PREFIX.matcher(line);
190196
if (matcher.find() && !LINE_COMMENT_NO_SPACE_PREFIX.matcher(line).find()) {

‎open-java-format/src/main/java/com/palantir/javaformat/java/JavaInputAstVisitor.java‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,10 +1247,10 @@ protected void visitLambdaExpression(LambdaExpressionTree node, boolean statemen
12471247
token(",");
12481248
builder.breakOp(" ");
12491249
}
1250-
visitVariables(
1251-
ImmutableList.of(parameter),
1252-
DeclarationKind.NONE,
1253-
inlineAnnotationDirection(parameter.getModifiers()));
1250+
// A lambda parameter is a parameter: its annotations stay on its line, as a method parameter's do. The
1251+
// vertical direction, a forced break after an annotation with arguments, is for local variable
1252+
// declarations.
1253+
visitVariables(ImmutableList.of(parameter), DeclarationKind.NONE, Direction.HORIZONTAL);
12541254
first = false;
12551255
}
12561256
if (parens) {

‎open-java-format/src/test/java/com/palantir/javaformat/java/FormatterTest.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.nio.file.Path;
3636
import java.time.Duration;
3737
import java.util.List;
38+
import org.junit.jupiter.api.Assumptions;
3839
import org.junit.jupiter.api.Test;
3940
import org.junit.jupiter.api.io.TempDir;
4041
import org.junit.jupiter.api.parallel.Execution;
@@ -445,6 +446,28 @@ void wrapMarkdownDocstringComment() throws Exception {
445446
assertThat(Formatter.create().formatSource(input)).isEqualTo(expected);
446447
}
447448

449+
@Test
450+
void wrapMarkdownDocstringRunKeepsTheSlashPrefix() throws Exception {
451+
// javac returns a run of `///` lines as one comment token from JDK 23 on; before that each line is its
452+
// own token and the wrapped line is always the first of its token, which is never misread.
453+
Assumptions.assumeTrue(
454+
Formatter.getRuntimeVersion() >= 23, "a `///` run is one comment token only from JDK 23 on");
455+
String input = "class T {\n"
456+
+ " /// Summary line.\n"
457+
+ " /// one long incredibly unbroken sentence moving from topic to topic so that no-one had a"
458+
+ " chance to interrupt; @Deprecated\n"
459+
+ " void m() {}\n"
460+
+ "}\n";
461+
String expected = "class T {\n"
462+
+ " /// Summary line.\n"
463+
+ " /// one long incredibly unbroken sentence moving from topic to topic so that no-one had a"
464+
+ " chance\n"
465+
+ " /// to interrupt; @Deprecated\n"
466+
+ " void m() {}\n"
467+
+ "}\n";
468+
assertThat(Formatter.create().formatSource(input)).isEqualTo(expected);
469+
}
470+
448471
@Test
449472
void dontWrapMoeLineComments() throws Exception {
450473
assertThat(Formatter.create()
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.javaformat.java;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import com.palantir.javaformat.java.JavaFormatterOptions.Style;
22+
import org.junit.jupiter.api.Test;
23+
24+
/**
25+
* Tests comment rewriting against the comment toks javac produces, without needing the JDK that produces them. From
26+
* JDK 23 on, a run of {@code ///} markdown lines (JEP 467) arrives as a single comment tok whose lines after the first
27+
* carry their source indentation; before 23 each {@code //} line is its own tok. Building the tok directly covers the
28+
* multi-line shape on any JDK.
29+
*/
30+
final class JavaCommentsHelperTest {
31+
32+
private static final int COLUMN = 4;
33+
34+
/** Rewrites a comment tok that sits at column {@link #COLUMN}, as javac 23 or later would report it. */
35+
private static String rewrite(String commentText, Style style) throws FormatterException {
36+
JavaFormatterOptions options =
37+
JavaFormatterOptions.builder().style(style).build();
38+
JavaInput.Tok tok = new JavaInput.Tok(0, commentText, commentText, 0, COLUMN, false, null);
39+
// The helper reads the line separator and the index of the first code token from the input. Any input whose
40+
// first token has index 0 will do: the tok above then counts as a comment inside the code, which is wrapped.
41+
JavaInput input = new JavaInput("class T {}\n");
42+
return new JavaCommentsHelper(input, options).rewrite(tok, options.maxLineLength(), COLUMN);
43+
}
44+
45+
/** A run of {@code ///} lines, indented as javac reports it, with {@code body} as the second line's text. */
46+
private static String markdownRun(String body) {
47+
return "/// Summary line.\n" + " ".repeat(COLUMN) + "/// " + body;
48+
}
49+
50+
private static String wordsOfLength(int length) {
51+
StringBuilder sb = new StringBuilder();
52+
while (sb.length() < length) {
53+
sb.append(sb.length() == 0 ? "" : " ").append("word");
54+
}
55+
return sb.substring(0, length);
56+
}
57+
58+
@Test
59+
void everyWrappedLineKeepsTheSlashPrefix() throws Exception {
60+
// Before the fix the prefix was read from the untrimmed continuation line, came back empty, and the
61+
// overflow was emitted as bare code -- which stops being a comment at all.
62+
String rewritten = rewrite(markdownRun(wordsOfLength(140) + " @Deprecated"), Style.OJF);
63+
64+
assertThat(rewritten).contains("\n");
65+
assertThat(rewritten.lines()).allSatisfy(line -> assertThat(line.trim()).startsWith("///"));
66+
assertThat(rewritten).doesNotContain("\n @Deprecated");
67+
}
68+
69+
@Test
70+
void aContinuationLineAtTheLimitIsNotWrapped() throws Exception {
71+
// The line's visual width is COLUMN + its trimmed length, so the budget is maxLineLength - COLUMN.
72+
int budget = Style.OJF.maxLineLength() - COLUMN;
73+
String body = wordsOfLength(budget - "/// ".length());
74+
75+
assertThat(rewrite(markdownRun(body), Style.OJF).lines()).hasSize(2);
76+
}
77+
78+
@Test
79+
void aContinuationLineOverTheLimitIsWrapped() throws Exception {
80+
int budget = Style.OJF.maxLineLength() - COLUMN;
81+
String body = wordsOfLength(budget - "/// ".length() + 1);
82+
83+
assertThat(rewrite(markdownRun(body), Style.OJF).lines()).hasSize(3);
84+
}
85+
86+
@Test
87+
void anUnbreakableTokenIsLeftLong() throws Exception {
88+
// There is nowhere to break, so the line stays over the limit rather than becoming an empty `///`
89+
// followed by a bare URL.
90+
String url = "https://example.com/" + "a".repeat(Style.OJF.maxLineLength());
91+
92+
String rewritten = rewrite(markdownRun(url), Style.OJF);
93+
94+
assertThat(rewritten.lines()).hasSize(2);
95+
assertThat(rewritten).contains("/// " + url);
96+
}
97+
98+
@Test
99+
void theMissingSpaceRuleAppliesToEveryLineOfTheRun() throws Exception {
100+
// Otherwise the same source formats differently depending on whether the running JDK hands the run
101+
// over as one tok (23 and later) or as one tok per line.
102+
String rewritten = rewrite("///Summary line.\n" + " ".repeat(COLUMN) + "///More text.", Style.OJF);
103+
104+
assertThat(rewritten).isEqualTo("/// Summary line.\n" + " ".repeat(COLUMN) + "/// More text.");
105+
}
106+
107+
@Test
108+
void anOrdinaryLineCommentStillWraps() throws Exception {
109+
String rewritten = rewrite("// " + wordsOfLength(140), Style.OJF);
110+
111+
assertThat(rewritten.lines()).hasSize(2);
112+
assertThat(rewritten.lines()).allSatisfy(line -> assertThat(line.trim()).startsWith("// "));
113+
}
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import java.util.function.BiFunction;
2+
import java.util.function.Consumer;
3+
import java.util.function.Function;
4+
5+
class AnnotatedLambdaParameters {
6+
BiFunction<Integer, Integer, Integer> withArgs =
7+
(final var x,
8+
@SuppressWarnings("unused")
9+
var y) -> x + y;
10+
11+
Function<String, Integer> typed = ( @SuppressWarnings( "unused" ) String s )->s.length();
12+
13+
BiFunction<Integer, Integer, Integer> two = (@Deprecated @SuppressWarnings("unused")
14+
var x, var y) -> x + y;
15+
16+
BiFunction<Integer, Integer, Integer> marker = (@Deprecated var x,
17+
@Deprecated final var y) -> x + y;
18+
19+
Consumer<String> block = (@SuppressWarnings({"unused", "rawtypes"})
20+
String s) -> { System.out.println(s); };
21+
22+
BiFunction<Integer, Integer, Integer> tooLongForOneLine = (@SuppressWarnings("unused") Integer first, @SuppressWarnings("unused") Integer second) -> first + second;
23+
24+
BiFunction<Integer, Integer, Integer> parametersDoNotFitOnOneLine = (@SuppressWarnings("unused") Integer firstParameterWithAVeryLongName, @SuppressWarnings("unused") Integer secondParameterWithAVeryLongName) -> firstParameterWithAVeryLongName + secondParameterWithAVeryLongName;
25+
26+
void method(@SuppressWarnings("unused") int y, @Deprecated int z) {
27+
@SuppressWarnings("unchecked") Function<Object, String> local = o -> (String) o;
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import java.util.function.BiFunction;
2+
import java.util.function.Consumer;
3+
import java.util.function.Function;
4+
5+
class AnnotatedLambdaParameters {
6+
BiFunction<Integer, Integer, Integer> withArgs = (final var x, @SuppressWarnings("unused") var y) -> x + y;
7+
8+
Function<String, Integer> typed = (@SuppressWarnings("unused") String s) -> s.length();
9+
10+
BiFunction<Integer, Integer, Integer> two = (@Deprecated @SuppressWarnings("unused") var x, var y) -> x + y;
11+
12+
BiFunction<Integer, Integer, Integer> marker = (@Deprecated var x, @Deprecated final var y) -> x + y;
13+
14+
Consumer<String> block = (@SuppressWarnings({"unused", "rawtypes"}) String s) -> {
15+
System.out.println(s);
16+
};
17+
18+
BiFunction<Integer, Integer, Integer> tooLongForOneLine =
19+
(@SuppressWarnings("unused") Integer first, @SuppressWarnings("unused") Integer second) -> first + second;
20+
21+
BiFunction<Integer, Integer, Integer> parametersDoNotFitOnOneLine =
22+
(@SuppressWarnings("unused") Integer firstParameterWithAVeryLongName,
23+
@SuppressWarnings("unused") Integer secondParameterWithAVeryLongName) ->
24+
firstParameterWithAVeryLongName + secondParameterWithAVeryLongName;
25+
26+
void method(@SuppressWarnings("unused") int y, @Deprecated int z) {
27+
@SuppressWarnings("unchecked")
28+
Function<Object, String> local = o -> (String) o;
29+
}
30+
}

0 commit comments

Comments
 (0)