Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
* <pre>{@code ...}</pre>} then the stack of nested contexts would be {@code PRE} plus {@code
* CODE_CONTEXT}.
*/
enum NestingContext {
/** {@code <pre>...</pre>}. */
PRE,

/** {@code {@code ...}}. */
CODE_CONTEXT,

/** {@code <table>...</table>}. */
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<NestingContext> contextStack = new NestingStack<>();
private boolean somethingSinceNewline;

private JavadocLexer(
Expand Down Expand Up @@ -200,56 +222,60 @@ private Function<String, Token> 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();
// <code>
contextStack.push(NestingContext.CODE_CONTEXT);
return preserveExistingFormatting ? Literal::new : CodeOpenTag::new;
} else if (input.tryConsumeRegex(CODE_CLOSE_PATTERN)) {
codeStack.popIfNotEmpty();
// </code>
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;
}

Expand Down Expand Up @@ -293,17 +319,11 @@ private Function<String, Token> 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();
}
}
Expand Down Expand Up @@ -535,6 +555,31 @@ private static void deindentPreCodeBlock(
}
}

/** Contexts that imply that we should not do HTML interpretation. */
private static final ImmutableSet<NestingContext> 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<NestingContext> 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<NestingContext> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,91 @@

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 <E> The type of the elements in the stack.
*/
final class NestingStack {
private int total;
private final Deque<Integer> stack = new ArrayDeque<>();
sealed class NestingStack<E> {
private final Deque<E> 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<E> values) {
return stack.stream().anyMatch(values::contains);
}

boolean isEmpty() {
return stack.isEmpty();
}

void reset() {
total = 0;
stack.clear();
}

static final class Int extends NestingStack<Integer> {
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,21 @@ public class B29368546 {
* foo bar
* }
*
* more stuff that ends with {
* }</pre>
* more stuff that ends with {}
* </pre>
*/
int x;

/**
* Example:
*
* <pre>{@code
* class T {}
* </pre> // oops, we forgot the close brace
* class T {
* }</pre>
*
* more stuff that ends with {}
* // oops, we forgot the close brace
*
* <p>more stuff that ends with {}
*/
int x;
}
Loading