From 4cf17dd7543e8ebfc4c7148846490832064f0e1e Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Thu, 8 Oct 2026 21:33:28 +0300 Subject: [PATCH 1/2] Join hyphenated words broken across lines in Javadoc comments. When a compound word was hyphenated across a line break in Javadoc (e.g., `infinite-\n * precision`), the formatter previously joined the tokens with a space, resulting in `infinite- precision`. Join adjacent word literals across a line break when the preceding literal ends in a hyphen following a word character, while preserving spaces after suspended hyphens (e.g., `pre- and post-`). Ported from google/google-java-format#1475 (commit 55863463), adapted to this fork's Token API. The markdown `///` test does not apply here. The tests were written first: hyphenatedLineBreakJoined failed with `infinite- precision`, suspendedHyphenPreserved and hyphenBeforeParagraphBreakNotJoined guard the cases that must stay as they are. --- .../javaformat/java/javadoc/JavadocLexer.java | 31 +++++++++-- .../java/JavadocFormattingTest.java | 53 +++++++++++++++++++ 2 files changed, 80 insertions(+), 4 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..2604627a6 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 @@ -339,6 +339,16 @@ private static ImmutableList joinAdjacentLiteralsAndAdjacentWhitespace(Li continue; } + if (seenWhitespace.indexOf("\n") >= 0 + && !hasMultipleNewlines(seenWhitespace.toString()) + && isHyphenatedWordEnd(accumulated) + && tokens.peek().getType() == LITERAL + && isWordStart(tokens.peek().getValue()) + && !isSuspendedHyphenConjunction(tokens.peek().getValue())) { + accumulated.append(tokens.next().getValue()); + continue; + } + output.add(new Token(LITERAL, accumulated.toString())); accumulated.setLength(0); @@ -350,13 +360,26 @@ private static ImmutableList joinAdjacentLiteralsAndAdjacentWhitespace(Li // We have another token coming, possibly of type OTHER. Leave it for the next iteration. } - /* - * TODO(cpovirk): Another case where we could try to join tokens is if a line ends with - * /[^ -]-/, as in "non-\nblocking." - */ return output.build(); } + private static boolean isHyphenatedWordEnd(CharSequence cs) { + int length = cs.length(); + return length >= 2 && cs.charAt(length - 1) == '-' && isWordChar(cs.charAt(length - 2)); + } + + private static boolean isWordChar(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + + private static boolean isWordStart(String s) { + return !s.isEmpty() && isWordChar(s.charAt(0)); + } + + private static boolean isSuspendedHyphenConjunction(String s) { + return s.equals("and") || s.equals("or"); + } + /** * Where the input has two consecutive line breaks between literals, insert a {@code

} tag between the literals. * 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..be68c551d 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 @@ -1496,4 +1496,57 @@ void snippetKeepsCommentsAndIndentation() { }; doFormatTest(input, input); } + + @Test + void hyphenatedLineBreakJoined() { + String[] input = { + "/**", // + " * This calculation requires an infinite-", + " * precision number.", + " */", + "class Test {}", + }; + String[] expected = { + "/** This calculation requires an infinite-precision number. */", // + "class Test {}", + }; + doFormatTest(input, expected); + } + + @Test + void suspendedHyphenPreserved() { + String[] input = { + "/**", // + " * Both pre-", + " * and post-processing steps.", + " */", + "class Test {}", + }; + String[] expected = { + "/** Both pre- and post-processing steps. */", // + "class Test {}", + }; + doFormatTest(input, expected); + } + + @Test + void hyphenBeforeParagraphBreakNotJoined() { + String[] input = { + "/**", // + " * Ends with a non-", + " *", + " * blocking paragraph.", + " */", + "class Test {}", + }; + String[] expected = { + "/**", // + " * Ends with a non-", + " *", + " *

blocking paragraph.", + " */", + "class Test {}", + }; + doFormatTest(input, expected); + } } From f695d7f95c03e5ce7df56bf04ee6a69645c5165a Mon Sep 17 00:00:00 2001 From: cpovirk Date: Thu, 8 Oct 2026 21:33:50 +0300 Subject: [PATCH 2/2] Drop whitespace requests before the first significant Javadoc token A block-level tag as the first content of a comment (

, 
    ,
    ,

    , , {@snippet}) requests a blank line before itself, and the writer honoured it right after the opening `/**` line, producing two empty ` *` lines at the top of the comment. Ignore any whitespace request while nothing significant has been written yet; the footer-tag special case for the same situation becomes redundant and goes. Ported from the classic-Javadoc part of google/google-java-format commit e4cef582 ("Improve interactions between wroteAnythingSignificant and whitespace requests"). Tests first: preBlockAsFirstContentGetsNoBlankLines and listAsFirstContentGetsNoBlankLines both failed with the two empty lines. --- .../java/javadoc/JavadocWriter.java | 10 ++++-- .../java/JavadocFormattingTest.java | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) 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..efcb73061 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 @@ -112,9 +112,7 @@ void writeFooterJavadocTagStart(Token token) { */ postWriteModifiedContinuingListCount.reset(); - if (!wroteAnythingSignificant) { - // Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides. - } else if (!continuingFooterTag) { + if (!continuingFooterTag) { // First footer tag after a body tag. requestBlankLine(); } else { @@ -319,6 +317,12 @@ private void writeToken(Token token) { requestNewline(); } + if (!wroteAnythingSignificant) { + // Nothing precedes the first token but the opening ∕✱✱ and its newline, so no requested + // whitespace (e.g., the blank line a
     or 
      asks for) belongs before it. + requestedWhitespace = NONE; + } + if (requestedWhitespace == BLANK_LINE && (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) { /* 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 be68c551d..1a5ce65a5 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 @@ -1549,4 +1549,37 @@ void hyphenBeforeParagraphBreakNotJoined() { }; doFormatTest(input, expected); } + + @Test + void preBlockAsFirstContentGetsNoBlankLines() { + String[] input = { + "/**", // + " *
      {@code",
      +            " * class Demo {",
      +            " *   int x;",
      +            " * }",
      +            " * }
      ", + " */", + "class Test {}", + }; + doFormatTest(input, input); + } + + @Test + void listAsFirstContentGetsNoBlankLines() { + String[] input = { + "/**
      • one
      • two
      */", // + "class Test {}", + }; + String[] expected = { + "/**", // + " *
        ", + " *
      • one", + " *
      • two", + " *
      ", + " */", + "class Test {}", + }; + doFormatTest(input, expected); + } }