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 68640a926..b79d78c81 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 @@ -25,6 +25,7 @@ import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.PeekingIterator; import com.google.googlejavaformat.java.javadoc.Token.BeginJavadoc; import com.google.googlejavaformat.java.javadoc.Token.BlockquoteCloseTag; @@ -103,14 +104,35 @@ private static String stripJavadocBeginAndEnd(String input) { return input.substring("/**".length(), input.length() - "*/".length()); } + /** + * An element of the nested contexts we might be in. For example, if we are inside {@code + *
{@code ...}
} then the stack of nested contexts would be {@code PRE} plus {@code + * CODE_CONTEXT}. + */ + enum NestingContext { + /** {@code
...
}. */ + PRE, + + /** {@code {@code ...}}. */ + CODE_CONTEXT, + + /** {@code ...
}. */ + TABLE, + + /** {@code {@snippet ...}}. */ + SNIPPET_CONTEXT, + + /** Nested braces within one of the other contexts. */ + BRACE_CONTEXT, + + /** {@code an inline tag such as {@link ...}} */ + TAG_CONTEXT + } + private final CharStream input; private final boolean classicJavadoc; private final MarkdownPositions markdownPositions; - private final NestingStack braceStack = new NestingStack(); - private final NestingStack preStack = new NestingStack(); - private final NestingStack codeStack = new NestingStack(); - private final NestingStack tableStack = new NestingStack(); - private boolean outerInlineTagIsSnippet; + private final NestingStack contextStack = new NestingStack<>(); private boolean somethingSinceNewline; private JavadocLexer( @@ -200,56 +222,60 @@ private Function consumeToken() throws LexException { somethingSinceNewline = true; if (input.tryConsumeRegex(SNIPPET_TAG_OPEN_PATTERN)) { - if (braceStack.isEmpty()) { - braceStack.push(); - outerInlineTagIsSnippet = true; - return SnippetBegin::new; - } - braceStack.push(); - return Literal::new; + // {@snippet ...} + boolean outermost = contextStack.isEmpty(); + contextStack.push(NestingContext.SNIPPET_CONTEXT); + return outermost ? SnippetBegin::new : Literal::new; } else if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) { - braceStack.push(); + // {@foo ...}. We recognize this even in something like {@code {@foo ...}}, but it doesn't + // make any difference. + contextStack.push(NestingContext.TAG_CONTEXT); return Literal::new; } else if (input.tryConsume("{")) { - braceStack.incrementIfPositive(); + // A left brace that is not the start of {@foo}. If we are inside another context, we'll + // record the brace, for cases like {@code foo{bar}}, where the second brace is the end of the + // tag. + if (contextStack.containsAny(BRACE_CONTEXTS)) { + contextStack.push(NestingContext.BRACE_CONTEXT); + } return Literal::new; } else if (input.tryConsume("}")) { - if (outerInlineTagIsSnippet && braceStack.total() == 1) { - braceStack.popIfNotEmpty(); - outerInlineTagIsSnippet = false; + var popped = contextStack.popIfNotEmpty(); + if (contextStack.isEmpty() && popped == NestingContext.SNIPPET_CONTEXT) { return SnippetEnd::new; } - braceStack.popIfNotEmpty(); return Literal::new; } // Inside an inline tag, don't do any HTML interpretation. - if (!braceStack.isEmpty()) { + if (contextStack.containsAny(TAG_CONTEXTS)) { verify(input.tryConsumeRegex(literalPattern())); return Literal::new; } if (input.tryConsumeRegex(PRE_OPEN_PATTERN)) { - preStack.push(); + contextStack.push(NestingContext.PRE); return preserveExistingFormatting ? Literal::new : PreOpenTag::new; } else if (input.tryConsumeRegex(PRE_CLOSE_PATTERN)) { - preStack.popIfNotEmpty(); + contextStack.popUntil(NestingContext.PRE); return preserveExistingFormatting() ? Literal::new : PreCloseTag::new; } if (input.tryConsumeRegex(CODE_OPEN_PATTERN)) { - codeStack.push(); + // + contextStack.push(NestingContext.CODE_CONTEXT); return preserveExistingFormatting ? Literal::new : CodeOpenTag::new; } else if (input.tryConsumeRegex(CODE_CLOSE_PATTERN)) { - codeStack.popIfNotEmpty(); + // + contextStack.popUntil(NestingContext.CODE_CONTEXT); return preserveExistingFormatting() ? Literal::new : CodeCloseTag::new; } if (input.tryConsumeRegex(TABLE_OPEN_PATTERN)) { - tableStack.push(); + contextStack.push(NestingContext.TABLE); return preserveExistingFormatting ? Literal::new : TableOpenTag::new; } else if (input.tryConsumeRegex(TABLE_CLOSE_PATTERN)) { - tableStack.popIfNotEmpty(); + contextStack.popUntil(NestingContext.TABLE); return preserveExistingFormatting() ? Literal::new : TableCloseTag::new; } @@ -293,17 +319,11 @@ private Function consumeToken() throws LexException { } private boolean preserveExistingFormatting() { - return !preStack.isEmpty() - || !tableStack.isEmpty() - || !codeStack.isEmpty() - || outerInlineTagIsSnippet; + return contextStack.containsAny(PRESERVE_FORMATTING_CONTEXTS); } private void checkMatchingTags() throws LexException { - if (!braceStack.isEmpty() - || !preStack.isEmpty() - || !tableStack.isEmpty() - || !codeStack.isEmpty()) { + if (!contextStack.isEmpty()) { throw new LexException(); } } @@ -535,6 +555,31 @@ private static void deindentPreCodeBlock( } } + /** Contexts that imply that we should not do HTML interpretation. */ + private static final ImmutableSet TAG_CONTEXTS = + ImmutableSet.of(NestingContext.SNIPPET_CONTEXT, NestingContext.TAG_CONTEXT); + + /** + * Contexts that are opened by a left brace and closed by a matching right brace. These are the + * ones where a nested left brace should open a nested context. + */ + private static final ImmutableSet BRACE_CONTEXTS = + ImmutableSet.of( + NestingContext.CODE_CONTEXT, + NestingContext.SNIPPET_CONTEXT, + NestingContext.BRACE_CONTEXT); + + /** + * Contexts that preserve formatting, including line breaks and leading whitespace, within the + * context. + */ + private static final ImmutableSet PRESERVE_FORMATTING_CONTEXTS = + ImmutableSet.of( + NestingContext.PRE, + NestingContext.TABLE, + NestingContext.CODE_CONTEXT, + NestingContext.SNIPPET_CONTEXT); + private static final CharMatcher NEWLINE = CharMatcher.is('\n'); private static boolean hasMultipleNewlines(String s) { 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 019e129fc..078fd44b6 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 @@ -65,9 +65,9 @@ final class JavadocWriter { private boolean continuingListItemOfInnermostList; private boolean continuingFooterTag; - private final NestingStack continuingListItemStack = new NestingStack(); - private final NestingStack continuingListStack = new NestingStack(); - private final NestingStack postWriteModifiedContinuingListStack = new NestingStack(); + private final NestingStack.Int continuingListItemStack = new NestingStack.Int(); + private final NestingStack.Int continuingListStack = new NestingStack.Int(); + private final NestingStack.Int postWriteModifiedContinuingListStack = new NestingStack.Int(); private int remainingOnLine; private boolean atStartOfLine; private RequestedWhitespace requestedWhitespace = NONE; diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java index c029428df..6bf201b3b 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java @@ -14,42 +14,51 @@ package com.google.googlejavaformat.java.javadoc; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.ArrayDeque; +import java.util.Collection; import java.util.Deque; +import org.jspecify.annotations.Nullable; /** - * Stack for tracking the level of nesting. In the simplest case, each entry is just the integer 1, - * and the stack is effectively a counter. In more complex cases, the entries may depend on context. - * For example, if the stack is keeping track of Javadoc lists, the entries represent indentation - * levels, and those depend on whether the list is an HTML list or a Markdown list. + * Stack for tracking the level of nesting. In the simplest case, we have a stack of {@link Integer} + * where each entry is just the integer 1, and the stack is effectively a counter. In more complex + * cases, the entries may depend on context. For example, if the stack is keeping track of Javadoc + * lists, the entries represent indentation levels, and those depend on whether the list is an HTML + * list or a Markdown list. + * + * @param The type of the elements in the stack. */ -final class NestingStack { - private int total; - private final Deque stack = new ArrayDeque<>(); +sealed class NestingStack { + private final Deque stack = new ArrayDeque<>(); - int total() { - return total; + void push(E value) { + stack.push(value); } - void push() { - push(1); + @CanIgnoreReturnValue + @Nullable E popIfNotEmpty() { + return isEmpty() ? null : stack.pop(); } - void push(int value) { - stack.push(value); - total += value; + /** + * If the stack contains the given element, pop it and everything above it. Otherwise, do nothing. + */ + void popUntil(E value) { + if (stack.contains(value)) { + E popped; + do { + popped = stack.pop(); + } while (!popped.equals(value)); + } } - void incrementIfPositive() { - if (total > 0) { - push(); - } + boolean contains(E value) { + return stack.contains(value); } - void popIfNotEmpty() { - if (!isEmpty()) { - total -= stack.pop(); - } + boolean containsAny(Collection values) { + return stack.stream().anyMatch(values::contains); } boolean isEmpty() { @@ -57,7 +66,39 @@ boolean isEmpty() { } void reset() { - total = 0; stack.clear(); } + + static final class Int extends NestingStack { + private int total; + + int total() { + return total; + } + + @Override + void push(Integer value) { + super.push(value); + total += value; + } + + void push() { + push(1); + } + + @Override + Integer popIfNotEmpty() { + Integer popped = super.popIfNotEmpty(); + if (popped != null) { + total -= popped; + } + return popped; + } + + @Override + void reset() { + super.reset(); + total = 0; + } + } } diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.output index 9f0fdd25e..654d63945 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.output +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.output @@ -43,8 +43,8 @@ public class B29368546 { * foo bar * } * - * more stuff that ends with { - * } + * more stuff that ends with {} + * */ int x; @@ -52,10 +52,12 @@ public class B29368546 { * Example: * *
{@code
-   * class T {}
-   * 
// oops, we forgot the close brace + * class T { + * } * - * more stuff that ends with {} + * // oops, we forgot the close brace + * + *

more stuff that ends with {} */ int x; }