Skip to content

Keep the comment prefix when wrapping a markdown docstring - #1776

Open
asm0dey wants to merge 2 commits into
palantir:developfrom
asm0dey:fix/markdown-docstring-wrapping
Open

asm0dey wants to merge 2 commits into
palantir:developfrom
asm0dey:fix/markdown-docstring-wrapping

Conversation

@asm0dey

@asm0dey asm0dey commented Sep 11, 2026

Copy link
Copy Markdown

Before this PR

From JDK 23 on, javac returns a run of /// markdown doc lines (JEP 467) as a single comment token, so every line after the first still carries its source indentation when JavaCommentsHelper.wrapLineComments sees it. Two lines there then go wrong:

// JavaCommentsHelper.java
String prefix = lineCommentPrefix(line);                       // reads '/' from index 0 of "    /// foo" -> ""
while (line.length() + column0 > options.maxLineLength()) {     // counts the indentation twice

The wrapped remainder is emitted with no /// at all. In Palantir style the result compiles, which is the dangerous part:

class P1 {
    /// Summary line.
    /// This second markdown line is long enough to overflow the palantir one hundred twenty column limit @Deprecated
    void f() {}
}

formats to

class P1 {
    /// Summary line.
    /// This second markdown line is long enough to overflow the palantir one hundred twenty column limit
    @Deprecated
    void f() {}
}

javap -v goes from 0 Deprecated markers to 4, javac exits 0, and nothing downstream notices that the formatter deprecated a method. In Google style the same input produces output that does not compile (illegal start of type), and formatting it again cannot parse it — so format followed by formatDiagnose/spotlessCheck fails on a file the formatter just wrote. Reproduced on JDK 23, 25 and 26, in full format and through formatSourceAndFixImports, which is what the Gradle plugin's Spotless step reaches via FormatterService.

It needs the /// block to be indented: at column0 == 0 the prefix is found correctly, which is why a top-level case does not reproduce it.

git log -S lineCommentPrefix dates the prefix variable to #1672. Before that commit the same input produced // limit @Deprecated — wrong documentation, but valid Java. #1672's test uses a single /// line, which is always line 0 of its token and always gets the right prefix, and it runs on the JDK 21 test task where the multi-line path is unreachable. For reference, google-java-format still hardcodes "// " here.

After this PR

The prefix and the width budget come from the trimmed line — which indentLineComments trims and re-indents anyway — so:

     /// Summary line.
-    /// …one hundred twenty column limit
-    @Deprecated
+    /// …one hundred and twenty column limit for sure
+    /// @Deprecated
     void f() {}
  • a continuation line keeps /// when it wraps;
  • a line at the column limit is no longer wrapped (the indentation was being counted twice);
  • an unbreakable token, e.g. a long URL, is left long instead of producing an empty /// followed by bare text;
  • ///foo → /// foo now applies to every line of the run, so the same source no longer formats differently depending on whether the running JDK hands the run over as one token or as one token per line.

Tests

JavaCommentsHelperTest builds the multi-line comment token directly instead of going through javac, so the JDK 21 test task covers a bug only a JDK 23 or later parser can produce. Four of its six tests fail without the one-line change; the other two are regression guards for ordinary // wrapping. The end-to-end case in FormatterTest is gated at 23 with an assumption.

Verified by hand with the CLI on JDK 25 and 26: the @Deprecated case stays inside the comment, javap -v reports 0 Deprecated markers, the output compiles, and formatting it again is a fixed point. ./gradlew build passes.

Not in this PR

The same investigation found more /// issues, all pre-existing and none of them code-corrupting. Listing them so they are not lost; happy to take any of them in follow-ups:

  • reflowing a line inside a fenced block, a {@snippet}, a markdown table or a link-reference definition changes the rendered markdown even when the wrap is syntactically safe — those regions should not be reflowed at all;
  • ///Summary. gaining a space raises the run's common indentation, which turns an indented code block into a paragraph (confirmed in the rendered HTML);
  • two trailing spaces, a CommonMark hard line break, are trimmed away;
  • doc/Comment.java:75-77 holds a second, unguarded copy of the missing-space rule, which also breaks a trailing //noinspection or //$NON-NLS-1$ — the exclusion regex in JavaCommentsHelper was written to prevent exactly that;
  • on JDK 21/22 an import referenced only from a markdown link (/// see [List]) is removed as unused, because getDocCommentTree returns nothing for /// there.

Split out of #1707, whose reviewer asked for these wrapping bugs to be a separate change. This PR does not depend on it and touches no file it touches.


AI Disclosure: This pull request was prepared with the assistance of Claude Code — the investigation, the fix, the tests, and this description — and reviewed by me before submission.

From JDK 23 on, javac returns a run of `///` lines (JEP 467) as a single comment token,
so every line after the first still carries its source indentation when
`JavaCommentsHelper.wrapLineComments` sees it. The prefix was read from index 0 of that
untrimmed line, came back empty, and the wrapped remainder was emitted with no `///` at
all -- as bare code. The same indentation was then counted twice in the width test, so
lines under the column limit were wrapped as well.

In Palantir style the result compiles, which is the dangerous part:

    class P1 {
        /// Summary line.
        /// This second markdown line is long enough to overflow the palantir one hundred twenty column limit @deprecated
        void f() {}
    }

formats to the comment, then `@Deprecated` on a line of its own, then the method.
`javap -v` goes from zero Deprecated markers to four, `javac` exits 0, and nothing
downstream notices that the formatter deprecated a method. In Google style the same
input produces output that does not compile (`illegal start of type`), and formatting it
again fails to parse. Both need the `///` block to be indented, which is why a
`column0 == 0` case does not reproduce it.

The prefix and the width budget now come from the trimmed line, which
`indentLineComments` trims and re-indents anyway. An unbreakable token is left long
instead of producing an empty `///` followed by bare text, and the missing-space rule
(`///foo` -> `/// foo`) now applies to every line of the run, so the same source no
longer formats differently depending on whether the running JDK hands the run over as
one token or as one token per line.

JavaCommentsHelperTest builds the multi-line comment token directly, so the JDK 21 test
task covers the bug that only a JDK 23 or later parser can produce; four of its six
tests fail without this change. The end-to-end test in FormatterTest is gated at 23.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changelog-app

changelog-app Bot commented Sep 11, 2026

Copy link
Copy Markdown

Generate changelog in changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

Keep the comment prefix when wrapping a markdown docstring

Check the box to generate changelog(s)

  • Generate changelog entry

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
abashev added a commit to openjavaformat/open-java-format that referenced this pull request Oct 3, 2026
…cstring-prefix

Keep the comment prefix when wrapping a markdown docstring (palantir palantir#1776)
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.

1 participant