Skip to content

Commit a7d2de6

Browse files
eamonnmcmanusgoogle-java-format Team
authored andcommitted
Replace the several nesting counters in JavadocLexer with a single stack.
I think this makes the logic a bit easier to follow, and possibly somewhat more accurate. One test case had to be updated to reflect the changes, in ways that are at least arguably an improvement. PiperOrigin-RevId: 897921255
1 parent a733cbc commit a7d2de6

4 files changed

Lines changed: 152 additions & 64 deletions

File tree

core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import com.google.common.base.CharMatcher;
2727
import com.google.common.collect.ImmutableList;
28+
import com.google.common.collect.ImmutableSet;
2829
import com.google.common.collect.PeekingIterator;
2930
import com.google.googlejavaformat.java.javadoc.Token.BeginJavadoc;
3031
import com.google.googlejavaformat.java.javadoc.Token.BlockquoteCloseTag;
@@ -103,14 +104,35 @@ private static String stripJavadocBeginAndEnd(String input) {
103104
return input.substring("/**".length(), input.length() - "*/".length());
104105
}
105106

107+
/**
108+
* An element of the nested contexts we might be in. For example, if we are inside {@code
109+
* <pre>{@code ...}</pre>} then the stack of nested contexts would be {@code PRE} plus {@code
110+
* CODE_CONTEXT}.
111+
*/
112+
enum NestingContext {
113+
/** {@code <pre>...</pre>}. */
114+
PRE,
115+
116+
/** {@code {@code ...}}. */
117+
CODE_CONTEXT,
118+
119+
/** {@code <table>...</table>}. */
120+
TABLE,
121+
122+
/** {@code {@snippet ...}}. */
123+
SNIPPET_CONTEXT,
124+
125+
/** Nested braces within one of the other contexts. */
126+
BRACE_CONTEXT,
127+
128+
/** {@code an inline tag such as {@link ...}} */
129+
TAG_CONTEXT
130+
}
131+
106132
private final CharStream input;
107133
private final boolean classicJavadoc;
108134
private final MarkdownPositions markdownPositions;
109-
private final NestingStack braceStack = new NestingStack();
110-
private final NestingStack preStack = new NestingStack();
111-
private final NestingStack codeStack = new NestingStack();
112-
private final NestingStack tableStack = new NestingStack();
113-
private boolean outerInlineTagIsSnippet;
135+
private final NestingStack<NestingContext> contextStack = new NestingStack<>();
114136
private boolean somethingSinceNewline;
115137

116138
private JavadocLexer(
@@ -200,56 +222,60 @@ private Function<String, Token> consumeToken() throws LexException {
200222
somethingSinceNewline = true;
201223

202224
if (input.tryConsumeRegex(SNIPPET_TAG_OPEN_PATTERN)) {
203-
if (braceStack.isEmpty()) {
204-
braceStack.push();
205-
outerInlineTagIsSnippet = true;
206-
return SnippetBegin::new;
207-
}
208-
braceStack.push();
209-
return Literal::new;
225+
// {@snippet ...}
226+
boolean outermost = contextStack.isEmpty();
227+
contextStack.push(NestingContext.SNIPPET_CONTEXT);
228+
return outermost ? SnippetBegin::new : Literal::new;
210229
} else if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) {
211-
braceStack.push();
230+
// {@foo ...}. We recognize this even in something like {@code {@foo ...}}, but it doesn't
231+
// make any difference.
232+
contextStack.push(NestingContext.TAG_CONTEXT);
212233
return Literal::new;
213234
} else if (input.tryConsume("{")) {
214-
braceStack.incrementIfPositive();
235+
// A left brace that is not the start of {@foo}. If we are inside another context, we'll
236+
// record the brace, for cases like {@code foo{bar}}, where the second brace is the end of the
237+
// tag.
238+
if (contextStack.containsAny(BRACE_CONTEXTS)) {
239+
contextStack.push(NestingContext.BRACE_CONTEXT);
240+
}
215241
return Literal::new;
216242
} else if (input.tryConsume("}")) {
217-
if (outerInlineTagIsSnippet && braceStack.total() == 1) {
218-
braceStack.popIfNotEmpty();
219-
outerInlineTagIsSnippet = false;
243+
var popped = contextStack.popIfNotEmpty();
244+
if (contextStack.isEmpty() && popped == NestingContext.SNIPPET_CONTEXT) {
220245
return SnippetEnd::new;
221246
}
222-
braceStack.popIfNotEmpty();
223247
return Literal::new;
224248
}
225249

226250
// Inside an inline tag, don't do any HTML interpretation.
227-
if (!braceStack.isEmpty()) {
251+
if (contextStack.containsAny(TAG_CONTEXTS)) {
228252
verify(input.tryConsumeRegex(literalPattern()));
229253
return Literal::new;
230254
}
231255

232256
if (input.tryConsumeRegex(PRE_OPEN_PATTERN)) {
233-
preStack.push();
257+
contextStack.push(NestingContext.PRE);
234258
return preserveExistingFormatting ? Literal::new : PreOpenTag::new;
235259
} else if (input.tryConsumeRegex(PRE_CLOSE_PATTERN)) {
236-
preStack.popIfNotEmpty();
260+
contextStack.popUntil(NestingContext.PRE);
237261
return preserveExistingFormatting() ? Literal::new : PreCloseTag::new;
238262
}
239263

240264
if (input.tryConsumeRegex(CODE_OPEN_PATTERN)) {
241-
codeStack.push();
265+
// <code>
266+
contextStack.push(NestingContext.CODE_CONTEXT);
242267
return preserveExistingFormatting ? Literal::new : CodeOpenTag::new;
243268
} else if (input.tryConsumeRegex(CODE_CLOSE_PATTERN)) {
244-
codeStack.popIfNotEmpty();
269+
// </code>
270+
contextStack.popUntil(NestingContext.CODE_CONTEXT);
245271
return preserveExistingFormatting() ? Literal::new : CodeCloseTag::new;
246272
}
247273

248274
if (input.tryConsumeRegex(TABLE_OPEN_PATTERN)) {
249-
tableStack.push();
275+
contextStack.push(NestingContext.TABLE);
250276
return preserveExistingFormatting ? Literal::new : TableOpenTag::new;
251277
} else if (input.tryConsumeRegex(TABLE_CLOSE_PATTERN)) {
252-
tableStack.popIfNotEmpty();
278+
contextStack.popUntil(NestingContext.TABLE);
253279
return preserveExistingFormatting() ? Literal::new : TableCloseTag::new;
254280
}
255281

@@ -293,17 +319,11 @@ private Function<String, Token> consumeToken() throws LexException {
293319
}
294320

295321
private boolean preserveExistingFormatting() {
296-
return !preStack.isEmpty()
297-
|| !tableStack.isEmpty()
298-
|| !codeStack.isEmpty()
299-
|| outerInlineTagIsSnippet;
322+
return contextStack.containsAny(PRESERVE_FORMATTING_CONTEXTS);
300323
}
301324

302325
private void checkMatchingTags() throws LexException {
303-
if (!braceStack.isEmpty()
304-
|| !preStack.isEmpty()
305-
|| !tableStack.isEmpty()
306-
|| !codeStack.isEmpty()) {
326+
if (!contextStack.isEmpty()) {
307327
throw new LexException();
308328
}
309329
}
@@ -535,6 +555,31 @@ private static void deindentPreCodeBlock(
535555
}
536556
}
537557

558+
/** Contexts that imply that we should not do HTML interpretation. */
559+
private static final ImmutableSet<NestingContext> TAG_CONTEXTS =
560+
ImmutableSet.of(NestingContext.SNIPPET_CONTEXT, NestingContext.TAG_CONTEXT);
561+
562+
/**
563+
* Contexts that are opened by a left brace and closed by a matching right brace. These are the
564+
* ones where a nested left brace should open a nested context.
565+
*/
566+
private static final ImmutableSet<NestingContext> BRACE_CONTEXTS =
567+
ImmutableSet.of(
568+
NestingContext.CODE_CONTEXT,
569+
NestingContext.SNIPPET_CONTEXT,
570+
NestingContext.BRACE_CONTEXT);
571+
572+
/**
573+
* Contexts that preserve formatting, including line breaks and leading whitespace, within the
574+
* context.
575+
*/
576+
private static final ImmutableSet<NestingContext> PRESERVE_FORMATTING_CONTEXTS =
577+
ImmutableSet.of(
578+
NestingContext.PRE,
579+
NestingContext.TABLE,
580+
NestingContext.CODE_CONTEXT,
581+
NestingContext.SNIPPET_CONTEXT);
582+
538583
private static final CharMatcher NEWLINE = CharMatcher.is('\n');
539584

540585
private static boolean hasMultipleNewlines(String s) {

core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ final class JavadocWriter {
6565
private boolean continuingListItemOfInnermostList;
6666

6767
private boolean continuingFooterTag;
68-
private final NestingStack continuingListItemStack = new NestingStack();
69-
private final NestingStack continuingListStack = new NestingStack();
70-
private final NestingStack postWriteModifiedContinuingListStack = new NestingStack();
68+
private final NestingStack.Int continuingListItemStack = new NestingStack.Int();
69+
private final NestingStack.Int continuingListStack = new NestingStack.Int();
70+
private final NestingStack.Int postWriteModifiedContinuingListStack = new NestingStack.Int();
7171
private int remainingOnLine;
7272
private boolean atStartOfLine;
7373
private RequestedWhitespace requestedWhitespace = NONE;

core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingStack.java

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,91 @@
1414

1515
package com.google.googlejavaformat.java.javadoc;
1616

17+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
1718
import java.util.ArrayDeque;
19+
import java.util.Collection;
1820
import java.util.Deque;
21+
import org.jspecify.annotations.Nullable;
1922

2023
/**
21-
* Stack for tracking the level of nesting. In the simplest case, each entry is just the integer 1,
22-
* and the stack is effectively a counter. In more complex cases, the entries may depend on context.
23-
* For example, if the stack is keeping track of Javadoc lists, the entries represent indentation
24-
* levels, and those depend on whether the list is an HTML list or a Markdown list.
24+
* Stack for tracking the level of nesting. In the simplest case, we have a stack of {@link Integer}
25+
* where each entry is just the integer 1, and the stack is effectively a counter. In more complex
26+
* cases, the entries may depend on context. For example, if the stack is keeping track of Javadoc
27+
* lists, the entries represent indentation levels, and those depend on whether the list is an HTML
28+
* list or a Markdown list.
29+
*
30+
* @param <E> The type of the elements in the stack.
2531
*/
26-
final class NestingStack {
27-
private int total;
28-
private final Deque<Integer> stack = new ArrayDeque<>();
32+
sealed class NestingStack<E> {
33+
private final Deque<E> stack = new ArrayDeque<>();
2934

30-
int total() {
31-
return total;
35+
void push(E value) {
36+
stack.push(value);
3237
}
3338

34-
void push() {
35-
push(1);
39+
@CanIgnoreReturnValue
40+
@Nullable E popIfNotEmpty() {
41+
return isEmpty() ? null : stack.pop();
3642
}
3743

38-
void push(int value) {
39-
stack.push(value);
40-
total += value;
44+
/**
45+
* If the stack contains the given element, pop it and everything above it. Otherwise, do nothing.
46+
*/
47+
void popUntil(E value) {
48+
if (stack.contains(value)) {
49+
E popped;
50+
do {
51+
popped = stack.pop();
52+
} while (!popped.equals(value));
53+
}
4154
}
4255

43-
void incrementIfPositive() {
44-
if (total > 0) {
45-
push();
46-
}
56+
boolean contains(E value) {
57+
return stack.contains(value);
4758
}
4859

49-
void popIfNotEmpty() {
50-
if (!isEmpty()) {
51-
total -= stack.pop();
52-
}
60+
boolean containsAny(Collection<E> values) {
61+
return stack.stream().anyMatch(values::contains);
5362
}
5463

5564
boolean isEmpty() {
5665
return stack.isEmpty();
5766
}
5867

5968
void reset() {
60-
total = 0;
6169
stack.clear();
6270
}
71+
72+
static final class Int extends NestingStack<Integer> {
73+
private int total;
74+
75+
int total() {
76+
return total;
77+
}
78+
79+
@Override
80+
void push(Integer value) {
81+
super.push(value);
82+
total += value;
83+
}
84+
85+
void push() {
86+
push(1);
87+
}
88+
89+
@Override
90+
Integer popIfNotEmpty() {
91+
Integer popped = super.popIfNotEmpty();
92+
if (popped != null) {
93+
total -= popped;
94+
}
95+
return popped;
96+
}
97+
98+
@Override
99+
void reset() {
100+
super.reset();
101+
total = 0;
102+
}
103+
}
63104
}

core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.output

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,21 @@ public class B29368546 {
4343
* foo bar
4444
* }
4545
*
46-
* more stuff that ends with {
47-
* }</pre>
46+
* more stuff that ends with {}
47+
* </pre>
4848
*/
4949
int x;
5050

5151
/**
5252
* Example:
5353
*
5454
* <pre>{@code
55-
* class T {}
56-
* </pre> // oops, we forgot the close brace
55+
* class T {
56+
* }</pre>
5757
*
58-
* more stuff that ends with {}
58+
* // oops, we forgot the close brace
59+
*
60+
* <p>more stuff that ends with {}
5961
*/
6062
int x;
6163
}

0 commit comments

Comments
 (0)