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;
}