From b19048ac98b5d1f932491cf7feb5856d42b166c7 Mon Sep 17 00:00:00 2001 From: Alex Abashev Date: Thu, 8 Oct 2026 21:43:33 +0300 Subject: [PATCH] Keep the relative indentation of
{@code} lines written
 without `*`

Lines inside a `
{@code ...}` block that omit the `*` margin lost all
their leading whitespace, because the lexer's newline pattern ate the
indentation of every continuation line. Code samples written this way (the
JDK's java.lang.invoke docs, for one) came out flush left.

The lexer now strips the margins line by line before tokenizing, as upstream
does in JavadocFormatter.classicCommentText: the `*` prefix and the space
after it from lines that have one, and the comment's own indentation from
lines that do not. That indentation is the smaller of the shortest `*` prefix
and the shortest bare-line indentation, so a bare line is never cut into and
both kinds of line stay aligned with each other. What remains is relative to
the comment, which is what deindentPreCodeBlocks expects. The newline pattern
shrinks to the trailing whitespace and the newline itself.

HTML comments were the one construct that still saw the raw margins, so the
writer now puts the margin back on each of their lines, as upstream's does;
commentMostlyUntouched takes upstream's current expectation (`* abc`,
`*   def`, `* -->`) instead of the raw `*abc`.

Resolves the same defect as google/google-java-format#1476, whose patch does
not apply here (no classicCommentText in this fork). Tests first:
preCodeWithoutLeadingStarPreservesIndent (upstream's case),
preCodeWithoutLeadingStarInIndentedComment (comment indented by four, the
base column must go) and preCodeMixedStarAndBareLinesKeepRelativeIndent all
failed with the sample flush left.

java.base of JDK 21 with Javadoc formatting on: 6 of 3,474 files change, all
of them bare-line code samples regaining their indentation.
---
 .../javaformat/java/javadoc/JavadocLexer.java |  50 ++++++++-
 .../java/javadoc/JavadocWriter.java           |  11 +-
 .../java/JavadocFormattingTest.java           | 102 +++++++++++++++++-
 3 files changed, 160 insertions(+), 3 deletions(-)

diff --git a/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocLexer.java b/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocLexer.java
index 78116960a..1cef3a8b1 100644
--- a/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocLexer.java
+++ b/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocLexer.java
@@ -55,12 +55,14 @@
 import static java.util.regex.Pattern.compile;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.PeekingIterator;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Deque;
 import java.util.List;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /** Lexer for the Javadoc formatter. */
@@ -74,9 +76,52 @@ static ImmutableList lex(String input) throws LexException {
          */
         input = stripJavadocBeginAndEnd(input);
         input = normalizeLineEndings(input);
+        input = stripMargins(input);
         return new JavadocLexer(new CharStream(input)).generateTokens();
     }
 
+    /**
+     * Strips the margin from every continuation line: the {@code *} prefix (with the space after it) from lines that
+     * have one, and the comment's own indentation from lines that do not. What remains of a bare line's indentation is
+     * relative to the comment, as on {@code *} lines after their {@code * }, so a {@code 
{@code} block written
+     * without the {@code *} margin keeps its shape for {@link #deindentPreCodeBlocks}.
+     *
+     * 

The indentation stripped from bare lines is the smaller of the shortest {@code *} prefix and the shortest + * indentation of a bare line, so a bare line is never cut into and lines of both kinds stay aligned with each + * other. + */ + private static String stripMargins(String input) { + List lines = Splitter.on('\n').splitToList(input); + List continuations = lines.subList(1, lines.size()); + int starPrefixLength = continuations.stream() + .map(STAR_PREFIX_PATTERN::matcher) + .filter(Matcher::find) + .mapToInt(Matcher::end) + .min() + .orElse(0); + int bareIndentation = continuations.stream() + .filter(line -> !STAR_PREFIX_PATTERN.matcher(line).find()) + .filter(NOT_SPACE_OR_TAB::matchesAnyOf) + .mapToInt(NOT_SPACE_OR_TAB::indexIn) + .min() + .orElse(0); + int bareStrip = Math.min(starPrefixLength, bareIndentation); + StringBuilder result = new StringBuilder(lines.get(0)); + for (String line : continuations) { + result.append('\n'); + Matcher star = STAR_PREFIX_PATTERN.matcher(line); + if (star.find()) { + result.append(line, star.end(), line.length()); + } else if (NOT_SPACE_OR_TAB.matchesAnyOf(line)) { + result.append(line, bareStrip, line.length()); + } + } + return result.toString(); + } + + private static final Pattern STAR_PREFIX_PATTERN = compile("^[ \t]*[*][ \t]?"); + private static final CharMatcher NOT_SPACE_OR_TAB = CharMatcher.noneOf(" \t"); + /** The lexer crashes on windows line endings, so for now just normalize to `\n`. */ // TODO(cushon): use the platform line separator for output private static String normalizeLineEndings(String input) { @@ -528,8 +573,11 @@ private static boolean hasMultipleNewlines(String s) { * * We'd remove the trailing whitespace later on (in JavaCommentsHelper.rewrite), but I feel safer * stripping it now: It otherwise might confuse our line-length count, which we use for wrapping. + * + * The margin of the next line (its `*` and the comment's indentation) is gone already: see + * stripMargins(). */ - private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n[ \t]*[*]?[ \t]?"); + private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n"); // We ensure elsewhere that we match this only at the beginning of a line. // Only match tags that start with a lowercase letter, to avoid false matches on unescaped diff --git a/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocWriter.java b/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocWriter.java index 884efb9f5..e86855742 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocWriter.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocWriter.java @@ -23,11 +23,13 @@ import static com.palantir.javaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE; import static com.palantir.javaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE; import static com.palantir.javaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG; +import static com.palantir.javaformat.java.javadoc.Token.Type.HTML_COMMENT; import static com.palantir.javaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG; import static com.palantir.javaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Ordering; +import java.util.List; import javax.annotation.Nullable; /** @@ -263,7 +265,14 @@ void writeMoeEndStripComment(Token token) { void writeHtmlComment(Token token) { requestNewline(); - writeToken(token); + // The lexer has stripped the margin from every line, so each one gets the margin back here. + List lines = token.getValue().lines().toList(); + writeToken(new Token(HTML_COMMENT, lines.get(0))); + for (String line : lines.subList(1, lines.size())) { + writeNewline(NO_AUTO_INDENT); + output.append(line); + remainingOnLine -= line.length(); + } requestNewline(); } diff --git a/open-java-format/src/test/java/com/palantir/javaformat/java/JavadocFormattingTest.java b/open-java-format/src/test/java/com/palantir/javaformat/java/JavadocFormattingTest.java index da51929fe..40bcb4ad5 100644 --- a/open-java-format/src/test/java/com/palantir/javaformat/java/JavadocFormattingTest.java +++ b/open-java-format/src/test/java/com/palantir/javaformat/java/JavadocFormattingTest.java @@ -109,7 +109,7 @@ void commentMostlyUntouched() { "/**", " * Foo.", " *", " * bar", " */", "class Test {}", }; String[] expected = { - "/**", " * Foo.", " * ", " * bar", " */", "class Test {}", + "/**", " * Foo.", " * ", " * bar", " */", "class Test {}", }; doFormatTest(input, expected); } @@ -1496,4 +1496,104 @@ void snippetKeepsCommentsAndIndentation() { }; doFormatTest(input, input); } + + @Test + void preCodeWithoutLeadingStarPreservesIndent() { + String[] input = { + "/**", // + " * Example:", + " *

{@code",
+            "class Demo {",
+            "  // Comment",
+            "  static final int X = 1;",
+            "",
+            "  public static void example() {",
+            "    int y = 2;",
+            "  }",
+            "}",
+            " * }
", + " */", + "class Test {}", + }; + String[] expected = { + "/**", // + " * Example:", + " *", + " *
{@code",
+            " * class Demo {",
+            " *   // Comment",
+            " *   static final int X = 1;",
+            " *",
+            " *   public static void example() {",
+            " *     int y = 2;",
+            " *   }",
+            " * }",
+            " * }
", + " */", + "class Test {}", + }; + doFormatTest(input, expected); + } + + @Test + void preCodeWithoutLeadingStarInIndentedComment() { + String[] input = { + "class Outer {", // + " /**", + " * Usage:", + " *
{@code",
+            "        Demo demo = new Demo();",
+            "        if (demo.ready()) {",
+            "            demo.run();",
+            "        }",
+            "     * }
", + " */", + " void m() {}", + "}", + }; + String[] expected = { + "class Outer {", // + " /**", + " * Usage:", + " *", + " *
{@code",
+            "   * Demo demo = new Demo();",
+            "   * if (demo.ready()) {",
+            "   *     demo.run();",
+            "   * }",
+            "   * }
", + " */", + " void m() {}", + "}", + }; + doFormatTest(input, expected); + } + + @Test + void preCodeMixedStarAndBareLinesKeepRelativeIndent() { + String[] input = { + "/**", // + " * Example:", + " *
{@code",
+            " * class Demo {",
+            "     int x;",
+            " * }",
+            " * }
", + " */", + "class Test {}", + }; + String[] expected = { + "/**", // + " * Example:", + " *", + " *
{@code",
+            " * class Demo {",
+            " *   int x;",
+            " * }",
+            " * }
", + " */", + "class Test {}", + }; + doFormatTest(input, expected); + } }