Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ private static String lineCommentPrefix(String line) {
private List<String> wrapLineComments(List<String> lines, int column0) {
List<String> result = new ArrayList<>();
for (String line : lines) {
// From JDK 23 on, javac returns a run of `///` markdown lines as a single comment tok, so
// every line after the first still carries its source indentation. indentLineComments
// trims and re-indents all of them, so both the slash prefix and the width budget have to
// be read from the trimmed text: otherwise the prefix comes back empty and the wrapped
// remainder is emitted as bare code.
line = CharMatcher.whitespace().trimLeadingFrom(line);
// Add missing leading spaces to line comments: `//foo` -> `// foo`.
Matcher matcher = LINE_COMMENT_MISSING_SPACE_PREFIX.matcher(line);
if (matcher.find()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.parallel.Execution;
Expand Down Expand Up @@ -418,6 +419,28 @@ void wrapMarkdownDocstringComment() throws Exception {
assertThat(Formatter.create().formatSource(input)).isEqualTo(expected);
}

@Test
void wrapMarkdownDocstringRunKeepsTheSlashPrefix() throws Exception {
// javac returns a run of `///` lines as one comment token from JDK 23 on; before that each line is its
// own token and the wrapped line is always the first of its token, which is never misread.
Assumptions.assumeTrue(
Formatter.getRuntimeVersion() >= 23, "a `///` run is one comment token only from JDK 23 on");
String input = "class T {\n"
+ " /// Summary line.\n"
+ " /// one long incredibly unbroken sentence moving from topic to topic so that no-one had a"
+ " chance to interrupt; @Deprecated\n"
+ " void m() {}\n"
+ "}\n";
String expected = "class T {\n"
+ " /// Summary line.\n"
+ " /// one long incredibly unbroken sentence moving from topic to topic so that no-one had a"
+ " chance\n"
+ " /// to interrupt; @Deprecated\n"
+ " void m() {}\n"
+ "}\n";
assertThat(Formatter.create().formatSource(input)).isEqualTo(expected);
}

@Test
void dontWrapMoeLineComments() throws Exception {
assertThat(Formatter.create()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.javaformat.java;

import static org.assertj.core.api.Assertions.assertThat;

import com.palantir.javaformat.java.JavaFormatterOptions.Style;
import org.junit.jupiter.api.Test;

/**
* Tests comment rewriting against the comment toks javac produces, without needing the JDK that produces them. From
* JDK 23 on, a run of {@code ///} markdown lines (JEP 467) arrives as a single comment tok whose lines after the first
* carry their source indentation; before 23 each {@code //} line is its own tok. Building the tok directly covers the
* multi-line shape on any JDK.
*/
final class JavaCommentsHelperTest {

private static final int COLUMN = 4;

/** Rewrites a comment tok that sits at column {@link #COLUMN}, as javac 23 or later would report it. */
private static String rewrite(String commentText, Style style) throws FormatterException {
JavaFormatterOptions options =
JavaFormatterOptions.builder().style(style).build();
JavaInput.Tok tok = new JavaInput.Tok(0, commentText, commentText, 0, COLUMN, false, null);
// The helper reads the line separator and the index of the first code token from the input. Any input whose
// first token has index 0 will do: the tok above then counts as a comment inside the code, which is wrapped.
JavaInput input = new JavaInput("class T {}\n");
return new JavaCommentsHelper(input, options).rewrite(tok, options.maxLineLength(), COLUMN);
}

/** A run of {@code ///} lines, indented as javac reports it, with {@code body} as the second line's text. */
private static String markdownRun(String body) {
return "/// Summary line.\n" + " ".repeat(COLUMN) + "/// " + body;
}

private static String wordsOfLength(int length) {
StringBuilder sb = new StringBuilder();
while (sb.length() < length) {
sb.append(sb.length() == 0 ? "" : " ").append("word");
}
return sb.substring(0, length);
}

@Test
void everyWrappedLineKeepsTheSlashPrefix() throws Exception {
// Before the fix the prefix was read from the untrimmed continuation line, came back empty, and the
// overflow was emitted as bare code -- which stops being a comment at all.
String rewritten = rewrite(markdownRun(wordsOfLength(140) + " @Deprecated"), Style.OJF);

assertThat(rewritten).contains("\n");
assertThat(rewritten.lines()).allSatisfy(line -> assertThat(line.trim()).startsWith("///"));
assertThat(rewritten).doesNotContain("\n @Deprecated");
}

@Test
void aContinuationLineAtTheLimitIsNotWrapped() throws Exception {
// The line's visual width is COLUMN + its trimmed length, so the budget is maxLineLength - COLUMN.
int budget = Style.OJF.maxLineLength() - COLUMN;
String body = wordsOfLength(budget - "/// ".length());

assertThat(rewrite(markdownRun(body), Style.OJF).lines()).hasSize(2);
}

@Test
void aContinuationLineOverTheLimitIsWrapped() throws Exception {
int budget = Style.OJF.maxLineLength() - COLUMN;
String body = wordsOfLength(budget - "/// ".length() + 1);

assertThat(rewrite(markdownRun(body), Style.OJF).lines()).hasSize(3);
}

@Test
void anUnbreakableTokenIsLeftLong() throws Exception {
// There is nowhere to break, so the line stays over the limit rather than becoming an empty `///`
// followed by a bare URL.
String url = "https://example.com/" + "a".repeat(Style.OJF.maxLineLength());

String rewritten = rewrite(markdownRun(url), Style.OJF);

assertThat(rewritten.lines()).hasSize(2);
assertThat(rewritten).contains("/// " + url);
}

@Test
void theMissingSpaceRuleAppliesToEveryLineOfTheRun() throws Exception {
// Otherwise the same source formats differently depending on whether the running JDK hands the run
// over as one tok (23 and later) or as one tok per line.
String rewritten = rewrite("///Summary line.\n" + " ".repeat(COLUMN) + "///More text.", Style.OJF);

assertThat(rewritten).isEqualTo("/// Summary line.\n" + " ".repeat(COLUMN) + "/// More text.");
}

@Test
void anOrdinaryLineCommentStillWraps() throws Exception {
String rewritten = rewrite("// " + wordsOfLength(140), Style.OJF);

assertThat(rewritten.lines()).hasSize(2);
assertThat(rewritten.lines()).allSatisfy(line -> assertThat(line.trim()).startsWith("// "));
}
}
Loading