From 762c6146dd0c6ff195e5bcab8eec708e8e3fd815 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Tue, 20 Aug 2024 15:00:07 -0700 Subject: [PATCH] Avoid mangling `{@snippet ...}`. This CL also implements some minimal formatting behavior _around_ `{@snippet ...}` (namely, a blank line before and after). It does not make any effort to change formatting _inside_ `{@snippet ...}`, only to stop reflowing it blindly. PiperOrigin-RevId: 665540906 --- .../java/javadoc/JavadocFormatter.java | 6 +++ .../java/javadoc/JavadocLexer.java | 25 ++++++++++- .../java/javadoc/JavadocWriter.java | 32 ++++++++++++++ .../googlejavaformat/java/javadoc/Token.java | 4 ++ .../java/JavadocFormattingTest.java | 42 +++++++++++++++++++ 5 files changed, 107 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java index 03938a677..4d45c9874 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java @@ -66,6 +66,12 @@ private static String render(List input, int blockIndent) { case FOOTER_JAVADOC_TAG_START: output.writeFooterJavadocTagStart(token); break; + case SNIPPET_BEGIN: + output.writeSnippetBegin(token); + break; + case SNIPPET_END: + output.writeSnippetEnd(token); + break; case LIST_OPEN_TAG: output.writeListOpen(token); break; diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java index cc707ae7e..d40f34c6b 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java @@ -42,6 +42,8 @@ import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_CLOSE_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_OPEN_TAG; +import static com.google.googlejavaformat.java.javadoc.Token.Type.SNIPPET_BEGIN; +import static com.google.googlejavaformat.java.javadoc.Token.Type.SNIPPET_END; import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_CLOSE_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_OPEN_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.WHITESPACE; @@ -97,6 +99,7 @@ private static String stripJavadocBeginAndEnd(String input) { private final NestingCounter preDepth = new NestingCounter(); private final NestingCounter codeDepth = new NestingCounter(); private final NestingCounter tableDepth = new NestingCounter(); + private boolean outerInlineTagIsSnippet; private boolean somethingSinceNewline; private JavadocLexer(CharStream input) { @@ -158,13 +161,26 @@ private Type consumeToken() throws LexException { } somethingSinceNewline = true; - if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) { + if (input.tryConsumeRegex(SNIPPET_TAG_OPEN_PATTERN)) { + if (braceDepth.value() == 0) { + braceDepth.increment(); + outerInlineTagIsSnippet = true; + return SNIPPET_BEGIN; + } + braceDepth.increment(); + return LITERAL; + } else if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) { braceDepth.increment(); return LITERAL; } else if (input.tryConsume("{")) { braceDepth.incrementIfPositive(); return LITERAL; } else if (input.tryConsume("}")) { + if (outerInlineTagIsSnippet && braceDepth.value() == 1) { + braceDepth.decrementIfPositive(); + outerInlineTagIsSnippet = false; + return SNIPPET_END; + } braceDepth.decrementIfPositive(); return LITERAL; } @@ -239,7 +255,10 @@ private Type consumeToken() throws LexException { } private boolean preserveExistingFormatting() { - return preDepth.isPositive() || tableDepth.isPositive() || codeDepth.isPositive(); + return preDepth.isPositive() + || tableDepth.isPositive() + || codeDepth.isPositive() + || outerInlineTagIsSnippet; } private void checkMatchingTags() throws LexException { @@ -400,6 +419,7 @@ private static ImmutableList optionalizeSpacesAfterLinks(List inpu *

Also trim leading and trailing blank lines, and move the trailing `}` to its own line. */ private static ImmutableList deindentPreCodeBlocks(List input) { + // TODO: b/323389829 - De-indent {@snippet ...} blocks, too. ImmutableList.Builder output = ImmutableList.builder(); for (PeekingIterator tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) { if (tokens.peek().getType() != PRE_OPEN_TAG) { @@ -528,6 +548,7 @@ private static boolean hasMultipleNewlines(String s) { private static final Pattern BLOCKQUOTE_OPEN_PATTERN = openTagPattern("blockquote"); private static final Pattern BLOCKQUOTE_CLOSE_PATTERN = closeTagPattern("blockquote"); private static final Pattern BR_PATTERN = openTagPattern("br"); + private static final Pattern SNIPPET_TAG_OPEN_PATTERN = compile("^[{]@snippet\\b"); private static final Pattern INLINE_TAG_OPEN_PATTERN = compile("^[{]@\\w*"); /* * We exclude < so that we don't swallow following HTML tags. This lets us fix up "foo

" (~400 diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java index 0361415a1..8a4100e45 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java @@ -123,6 +123,38 @@ void writeFooterJavadocTagStart(Token token) { continuingFooterTag = true; } + void writeSnippetBegin(Token token) { + requestBlankLine(); + writeToken(token); + /* + * We don't request a newline here because we should have at least a colon following on this + * line, and we may have attributes after that. + * + * (If we find it convenient, we could instead consume the entire rest of the line as part of + * the same token as `{@snippet` itself. But we already would never split the rest of the line + * across lines (because we preserve whitespace), so that might not accomplish anything. Plus, + * we'd probably want to be careful not to swallow an expectedly early closing `}`.) + */ + } + + void writeSnippetEnd(Token token) { + /* + * We don't request a newline here because we have preserved all newlines that existed in the + * input. TODO: b/323389829 - Improve upon that. Specifically: + * + * - If there is not yet a newline, we should add one. + * + * - If there are multiple newlines, we should probably collapse them. + * + * - If the closing brace isn't indented as we'd want (as in the link below, in which the whole + * @apiNote isn't indented), we should indent it. + * + * https://github.com/openjdk/jdk/blob/1ebf2cf639300728ffc024784f5dc1704317b0b3/src/java.base/share/classes/java/util/Collections.java#L5993-L6006 + */ + writeToken(token); + requestBlankLine(); + } + void writeListOpen(Token token) { requestBlankLine(); diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java index d617824b0..f74996060 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java @@ -41,6 +41,10 @@ enum Type { END_JAVADOC, /** The {@code @foo} that begins a block Javadoc tag like {@code @throws}. */ FOOTER_JAVADOC_TAG_START, + /** The opening {@code {@snippet} of a code snippet. */ + SNIPPET_BEGIN, + /** The closing {@code }} of a code snippet. */ + SNIPPET_END, LIST_OPEN_TAG, LIST_CLOSE_TAG, LIST_ITEM_OPEN_TAG, diff --git a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java index 6849c01f2..aab8ec5d4 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java @@ -906,6 +906,48 @@ public void unicodeCharacterCountArguableBug() { doFormatTest(input, expected); } + @Test + public void blankLinesAroundSnippetAndNoMangling() { + String[] input = { + "/**", // + " * hello world", + " * {@snippet :", + " * public class Foo {", + " * private String s;", + " * }", + " * }", + " * hello again", + " */", + "class Test {}", + }; + String[] expected = { + "/**", // + " * hello world", + " *", + " * {@snippet :", + " * public class Foo {", + " * private String s;", + " * }", + " * }", + " *", + " * hello again", + " */", + "class Test {}", + }; + doFormatTest(input, expected); + } + + @Test + public void notASnippetUnlessOuterTag() { + String[] input = { + "/** I would like to tell you about the {@code {@snippet ...}} tag. */", "class Test {}", + }; + String[] expected = { + "/** I would like to tell you about the {@code {@snippet ...}} tag. */", "class Test {}", + }; + doFormatTest(input, expected); + } + @Test public void blankLineBeforeParams() { String[] input = {